mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-09 03:58:55 +01:00
Vulkan: parity with GL (P1–P7), shared GPU culling records (N6c), threaded command recording and render thread (M1, M4)
This commit is contained in:
95
rendering/common/exposure.cpp
Normal file
95
rendering/common/exposure.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/exposure.h"
|
||||
|
||||
#include "gl/ubo.h"
|
||||
#include "utilities/Globals.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// luminance range of the histogram, log2: 2^-12 .. 2^6. darker pixels count as black, brighter ones as the brightest
|
||||
float const HISTOGRAM_LOG_MIN{-12.f};
|
||||
float const HISTOGRAM_LOG_MAX{6.f};
|
||||
// the bloom chain goes down this many levels at most, and not below this size
|
||||
std::size_t const BLOOM_LEVELS{6};
|
||||
int const BLOOM_MIN_SIZE{8};
|
||||
// soft knee of the threshold, a fraction of it
|
||||
float const BLOOM_KNEE{0.5f};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
glm::vec2 exposure_settings::histogram_range()
|
||||
{
|
||||
auto const bins{static_cast<float>(gl::EXPOSURE_HISTOGRAM_BINS - 2)};
|
||||
return glm::vec2{HISTOGRAM_LOG_MIN, bins / (HISTOGRAM_LOG_MAX - HISTOGRAM_LOG_MIN)};
|
||||
}
|
||||
|
||||
glm::vec2 exposure_settings::average_range()
|
||||
{
|
||||
auto const bins{static_cast<float>(gl::EXPOSURE_HISTOGRAM_BINS - 2)};
|
||||
return glm::vec2{HISTOGRAM_LOG_MIN, (HISTOGRAM_LOG_MAX - HISTOGRAM_LOG_MIN) / bins};
|
||||
}
|
||||
|
||||
glm::vec4 exposure_settings::adaptation(double const Deltatime)
|
||||
{
|
||||
return glm::vec4{static_cast<float>(std::clamp(Deltatime, 0.0, 1.0)), Global.gfx_postfx_autoexposure_speed_dark, Global.gfx_postfx_autoexposure_speed_bright, Global.gfx_postfx_autoexposure_key};
|
||||
}
|
||||
|
||||
glm::vec2 exposure_settings::exposure_range()
|
||||
{
|
||||
return glm::vec2{std::exp2(Global.gfx_postfx_autoexposure_min), std::exp2(Global.gfx_postfx_autoexposure_max)};
|
||||
}
|
||||
|
||||
std::vector<glm::ivec2> exposure_settings::bloom_sizes(int const Width, int const Height)
|
||||
{
|
||||
std::vector<glm::ivec2> sizes;
|
||||
glm::ivec2 size{Width, Height};
|
||||
for (std::size_t level = 0; level < BLOOM_LEVELS; ++level)
|
||||
{
|
||||
size = glm::max(size / 2, glm::ivec2{1});
|
||||
if (level > 0 && std::min(size.x, size.y) < BLOOM_MIN_SIZE)
|
||||
break;
|
||||
sizes.push_back(size);
|
||||
}
|
||||
return sizes;
|
||||
}
|
||||
|
||||
glm::vec4 exposure_settings::bloom_params(std::size_t const Level)
|
||||
{
|
||||
return glm::vec4{(Level == 0 ? 1.f : 0.f), Global.gfx_postfx_bloom_threshold, BLOOM_KNEE, 0.f};
|
||||
}
|
||||
|
||||
glm::vec4 exposure_settings::tonemap_params(std::size_t const Bloomlevels)
|
||||
{
|
||||
auto const bloom{(Global.gfx_postfx_bloom && Bloomlevels > 0) ? Global.gfx_postfx_bloom_intensity : 0.f};
|
||||
return glm::vec4{bloom, Global.gfx_postfx_autoexposure ? 1.f : 0.f, std::exp2(Global.gfx_postfx_autoexposure_compensation), Bloomlevels > 0 ? 1.f / static_cast<float>(Bloomlevels) : 0.f};
|
||||
}
|
||||
|
||||
void exposure_settings::draw_debug_ui(float const *Readback)
|
||||
{
|
||||
ImGui::Checkbox("automatic exposure (gfx.postfx.autoexposure)", &Global.gfx_postfx_autoexposure);
|
||||
ImGui::SliderFloat("key", &Global.gfx_postfx_autoexposure_key, 0.02f, 2.f, "%.3f", ImGuiSliderFlags_Logarithmic);
|
||||
ImGui::SliderFloat("min EV", &Global.gfx_postfx_autoexposure_min, -4.f, 0.f);
|
||||
ImGui::SliderFloat("max EV", &Global.gfx_postfx_autoexposure_max, 0.f, 4.f);
|
||||
ImGui::SliderFloat("adaptation to dark (1/s)", &Global.gfx_postfx_autoexposure_speed_dark, 0.05f, 10.f);
|
||||
ImGui::SliderFloat("adaptation to bright (1/s)", &Global.gfx_postfx_autoexposure_speed_bright, 0.05f, 10.f);
|
||||
ImGui::SliderFloat("compensation EV", &Global.gfx_postfx_autoexposure_compensation, -4.f, 4.f);
|
||||
ImGui::Text("average luminance %.3f, adapted %.3f, exposure %.3f (%+.2f EV)", Readback[2], Readback[0], Readback[1], std::log2(std::max(Readback[1], 1e-6f)));
|
||||
ImGui::Checkbox("bloom (gfx.postfx.bloom)", &Global.gfx_postfx_bloom);
|
||||
ImGui::SliderFloat("bloom intensity", &Global.gfx_postfx_bloom_intensity, 0.f, 0.5f);
|
||||
ImGui::SliderFloat("bloom threshold", &Global.gfx_postfx_bloom_threshold, 0.f, 10.f);
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
44
rendering/common/exposure.h
Normal file
44
rendering/common/exposure.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
// what the automatic exposure and the bloom of the renderers share (the part of the stage S3 of
|
||||
// claude/gl46-renderer-features-plan.md which isn't the work of the gpu, taken out of opengl46_exposure for the vulkan renderer,
|
||||
// stage P1 of claude/vulkan14-renderer-plan.md): the parameters of the histogram and of the adaptation
|
||||
// (exposure_histogram.comp, exposure_average.comp), the sizes of the levels of the bloom chain and the parameters of its steps
|
||||
// (postfx_bloom_*.frag), the parameters of the tonemapping and the controls of the debug window
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
struct exposure_settings
|
||||
{
|
||||
// x: log2 of the lowest luminance binned, y: bins per unit of log2 luminance (exposure_histogram.comp)
|
||||
static glm::vec2 histogram_range();
|
||||
// x: log2 of the lowest luminance binned, y: log2 luminance per bin (exposure_average.comp)
|
||||
static glm::vec2 average_range();
|
||||
// x: time since the last measurement (Deltatime, s), y, z: rates of the adaptation to a darker and a brighter image, w: key
|
||||
static glm::vec4 adaptation(double const Deltatime);
|
||||
// the lowest and the highest exposure (multipliers)
|
||||
static glm::vec2 exposure_range();
|
||||
// the sizes of the levels of the bloom chain of an image, half the size each, as many as there are (empty: no chain)
|
||||
static std::vector<glm::ivec2> bloom_sizes(int const Width, int const Height);
|
||||
// param[0] of a step down the chain (postfx_bloom_down.frag): Level 0 is the first one, from the image itself
|
||||
static glm::vec4 bloom_params(std::size_t const Level);
|
||||
// parameters of the tonemapping (postfx_tonemapping.frag), with bloom from a chain of Bloomlevels levels (0: none)
|
||||
static glm::vec4 tonemap_params(std::size_t const Bloomlevels);
|
||||
// controls of the debug window; Readback: the adapted luminance, the exposure and the average luminance the gpu worked out
|
||||
static void draw_debug_ui(float const *Readback);
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
321
rendering/common/gpuparticles.cpp
Normal file
321
rendering/common/gpuparticles.cpp
Normal file
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/gpuparticles.h"
|
||||
|
||||
#include "rendering/particles.h"
|
||||
#include "simulation/simulationenvironment.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "utilities/Timer.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
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};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool gpu_particles::init()
|
||||
{
|
||||
if (m_state != 0)
|
||||
return m_state > 0;
|
||||
m_state = -1;
|
||||
if (false == gpu_init())
|
||||
return false;
|
||||
m_state = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gpu_particles::smoke_available()
|
||||
{
|
||||
return init();
|
||||
}
|
||||
|
||||
bool gpu_particles::precipitation_available()
|
||||
{
|
||||
return init();
|
||||
}
|
||||
|
||||
void gpu_particles::clear()
|
||||
{
|
||||
m_pool.clear();
|
||||
m_requested.clear();
|
||||
m_poolfull = false;
|
||||
m_sourcerecords.clear();
|
||||
m_places = 0;
|
||||
m_storage = false;
|
||||
gpu_clear();
|
||||
m_precipitationcapacity = 0;
|
||||
m_originset = false;
|
||||
m_lastupdate = 0;
|
||||
}
|
||||
|
||||
void 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_storage)
|
||||
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));
|
||||
|
||||
// the particles of the places kept go over to the new storage, the new places are dead
|
||||
auto const places{std::max<std::uint32_t>(m_places, 1)};
|
||||
gpu_layout(places, (m_storage && (oldplaces > 0) && (kept > 0)) ? oldplaces : 0, owners, sources.size());
|
||||
m_storage = true;
|
||||
m_sourcerecords.resize(sources.size(), gl::particle_smoke_source{});
|
||||
}
|
||||
|
||||
void 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], state.spread};
|
||||
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.settling = glm::vec4{state.settling, state.drag, state.settles ? 1.f : 0.f, state.fadein};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
smoke_simulation simulation;
|
||||
simulation.places = m_places;
|
||||
simulation.step = glm::vec4{static_cast<float>(Particles.timedelta()), shift.x, shift.y, shift.z};
|
||||
auto const wind{glm::vec3{simulation::Environment.wind()}};
|
||||
simulation.wind = glm::vec4{wind.x, wind.y, wind.z, 0.f};
|
||||
simulation.seed = static_cast<std::uint32_t>(m_lastupdate);
|
||||
gpu_simulate(simulation, m_sourcerecords);
|
||||
}
|
||||
|
||||
std::array<glm::vec4, 5> 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;
|
||||
}
|
||||
|
||||
std::size_t gpu_particles::render_smoke(view const &View)
|
||||
{
|
||||
if ((false == init()) || (m_places == 0))
|
||||
return 0;
|
||||
smoke_draw draw;
|
||||
draw.places = m_places;
|
||||
draw.origin = glm::vec3{m_origin - View.camera};
|
||||
draw.view = View.modelview;
|
||||
draw.options = glm::vec4{Global.gfx_smoke_lighting ? 1.f : 0.f, SMOKE_FORWARD, static_cast<float>(m_places), 0.f};
|
||||
draw.planes = planes(View);
|
||||
draw.soft = glm::vec4{View.soft ? Global.gfx_particles_soft : 0.f, 0.f, 0.f, 0.f};
|
||||
gpu_render_smoke(draw, m_smokedrawn);
|
||||
return m_smokedrawn;
|
||||
}
|
||||
|
||||
std::size_t gpu_particles::render_precipitation(view const &View, bool const Snow, float const Intensity, unsigned int const Frame)
|
||||
{
|
||||
if (false == init())
|
||||
return 0;
|
||||
gpu_collect_precipitation(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);
|
||||
gpu_precipitation_storage(m_precipitationcapacity);
|
||||
}
|
||||
|
||||
precipitation_draw draw;
|
||||
draw.count = count;
|
||||
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]))};
|
||||
draw.counts = glm::uvec4{second, third, count, Snow ? 1u : 0u};
|
||||
for (std::size_t box = 0; box < BOX_SIZE.size(); ++box)
|
||||
{
|
||||
auto const size{static_cast<double>(BOX_SIZE[box])};
|
||||
draw.offsets[box] = glm::vec4{glm::vec3{glm::mod(m_drift[box] - glm::mod(View.camera, size), size)}, static_cast<float>(m_fall[box])};
|
||||
}
|
||||
draw.sizes = glm::vec4{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}};
|
||||
draw.velocity = glm::vec4{velocity.x, velocity.y, velocity.z, kind.fall};
|
||||
draw.timing = glm::vec4{static_cast<float>(m_time), kind.exposure, kind.opacity, static_cast<float>(m_precipitationcapacity)};
|
||||
draw.view = View.modelview;
|
||||
draw.options = glm::vec4{1.f, kind.forward, kind.brightness, 0.f};
|
||||
draw.planes = planes(View);
|
||||
// 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};
|
||||
draw.soft = glm::vec4{View.soft ? 0.25f * Global.gfx_particles_soft : 0.f, 0.f, 0.f, 0.f};
|
||||
draw.pixel = glm::vec4{pixel, 0.f, 0.f, 0.f};
|
||||
gpu_render_precipitation(draw);
|
||||
return m_precipitationdrawn;
|
||||
}
|
||||
|
||||
std::string 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;
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
185
rendering/common/gpuparticles.h
Normal file
185
rendering/common/gpuparticles.h
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
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 <string>
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "gl/ubo.h"
|
||||
|
||||
class particle_manager;
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
// particles of the gpu, whichever api draws them: 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. what the passes are given is worked out here; the buffers, the compute
|
||||
// passes and the draws are the renderer's (the gpu_ functions)
|
||||
class gpu_particles
|
||||
{
|
||||
public:
|
||||
gpu_particles() = default;
|
||||
virtual ~gpu_particles() = default;
|
||||
gpu_particles(gpu_particles const &) = delete;
|
||||
gpu_particles &operator=(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 the depth of the scene of the pass
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
protected:
|
||||
// the uniforms of particles_smoke_simulate.comp
|
||||
struct smoke_simulation
|
||||
{
|
||||
std::uint32_t places{0};
|
||||
glm::vec4 step{0.f}; // x: seconds of the update, yzw: added to the positions, as the origin moved
|
||||
glm::vec4 wind{0.f};
|
||||
std::uint32_t seed{0};
|
||||
};
|
||||
// the uniforms of particles_smoke_prepare.comp and of the draw
|
||||
struct smoke_draw
|
||||
{
|
||||
std::uint32_t places{0};
|
||||
glm::vec3 origin{0.f}; // of the particles less the camera
|
||||
glm::mat4 view{1.f};
|
||||
glm::vec4 options{0.f}; // x: lit by the lights of the pass, y: scattering onwards, z: places for the draws
|
||||
std::array<glm::vec4, 5> planes{};
|
||||
glm::vec4 soft{0.f}; // of particles_smoke.vert
|
||||
};
|
||||
// the uniforms of particles_precipitation.comp and of the draw
|
||||
struct precipitation_draw
|
||||
{
|
||||
glm::uvec4 counts{0u};
|
||||
std::array<glm::vec4, 3> offsets{};
|
||||
glm::vec4 sizes{0.f};
|
||||
glm::vec4 velocity{0.f};
|
||||
glm::vec4 timing{0.f};
|
||||
glm::mat4 view{1.f};
|
||||
glm::vec4 options{0.f};
|
||||
std::array<glm::vec4, 5> planes{};
|
||||
glm::vec4 soft{0.f}; // of particles_precipitation.vert
|
||||
glm::vec4 pixel{0.f};
|
||||
std::uint32_t count{0}; // particles, the threads of the compute pass
|
||||
};
|
||||
|
||||
// the programs; false: the particles of the gpu can't be used
|
||||
virtual bool gpu_init() = 0;
|
||||
// new storage of the smoke: Places places (at least 1) of particles, all dead but the first Kept of the storage before, which
|
||||
// keep what they had; the storage of the draws for as many particles, the source of every place (Owners) and room for the
|
||||
// records of Sources sources
|
||||
virtual void gpu_layout(std::uint32_t const Places, std::uint32_t const Kept, std::vector<std::uint32_t> const &Owners, std::size_t const Sources) = 0;
|
||||
// the records of the sources (when there are any) and the compute pass of the simulation
|
||||
virtual void gpu_simulate(smoke_simulation const &Simulation, std::vector<gl::particle_smoke_source> const &Records) = 0;
|
||||
// the compute pass and the draw of the smoke the view sees; Drawn: the particles drawn, as the last read back gives them (left
|
||||
// as it is when nothing came back yet)
|
||||
virtual void gpu_render_smoke(smoke_draw const &Draw, std::size_t &Drawn) = 0;
|
||||
// the particles of the rain or snow drawn, as the last read back gives them
|
||||
virtual void gpu_collect_precipitation(std::size_t &Drawn) = 0;
|
||||
// room for the draws of Capacity particles of the rain or snow
|
||||
virtual void gpu_precipitation_storage(std::uint32_t const Capacity) = 0;
|
||||
// the compute pass and the draw of the rain or snow
|
||||
virtual void gpu_render_precipitation(precipitation_draw const &Draw) = 0;
|
||||
// the storage goes (the draw commands and the read backs stay)
|
||||
virtual void gpu_clear() = 0;
|
||||
|
||||
std::uint32_t places() const
|
||||
{
|
||||
return m_places;
|
||||
}
|
||||
std::uint32_t precipitation_count() const
|
||||
{
|
||||
return m_precipitationcount;
|
||||
}
|
||||
bool precipitation_snow() const
|
||||
{
|
||||
return m_precipitationsnow;
|
||||
}
|
||||
|
||||
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);
|
||||
// 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);
|
||||
|
||||
int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used
|
||||
|
||||
// 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
|
||||
bool m_storage{false}; // the storage of the smoke was made
|
||||
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
|
||||
|
||||
// rain and snow
|
||||
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};
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
269
rendering/common/grassfield.cpp
Normal file
269
rendering/common/grassfield.cpp
Normal file
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/grassfield.h"
|
||||
|
||||
#include "simulation/simulationenvironment.h"
|
||||
#include "simulation/simulationtime.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Timer.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int constexpr max_blades{65536}; // drawn in a tile at most
|
||||
float constexpr blade_reach{1.5f}; // metres the bounds of a tile reach above its ground, for the blades
|
||||
|
||||
// the share of the blades drawn at specified distance: fewer further away, for speed (from Thinning), and fewer and fewer from
|
||||
// Fade to Reach, where the grass ends; the same as lod_share() of grass.vert
|
||||
float lod_share(float const Distance, float const Thinning, float const Fade, float const Reach)
|
||||
{
|
||||
auto const share{Distance <= Thinning ? 1.f : std::pow(Thinning / Distance, 1.5f)};
|
||||
auto const t{std::clamp((Distance - Fade) / std::max(Reach - Fade, 1e-3f), 0.f, 1.f)};
|
||||
return share * (1.f - t * t * (3.f - 2.f * t));
|
||||
}
|
||||
|
||||
// the grass through the year, by the day of the year: x how dry it is, y how fresh (the green of spring), z how grown (share
|
||||
// of the height). the turns follow the seasons of the textures (world_environment::compute_season(): winter till day 65,
|
||||
// spring till 158, summer till 252, autumn till 341): dormant and dry in winter, greening up and growing through spring,
|
||||
// fresh in May, drying out from August, the dry grass lying lower towards winter
|
||||
glm::vec3 grass_season(int const Yearday)
|
||||
{
|
||||
struct point
|
||||
{
|
||||
float day;
|
||||
glm::vec3 value;
|
||||
};
|
||||
static point const year[]{{1.f, {0.80f, 0.f, 0.55f}}, {65.f, {0.80f, 0.f, 0.55f}}, {100.f, {0.45f, 0.3f, 0.70f}}, {135.f, {0.05f, 1.f, 0.90f}},
|
||||
{170.f, {0.f, 0.4f, 1.f}}, {200.f, {0.10f, 0.f, 1.f}}, {240.f, {0.35f, 0.f, 1.f}}, {280.f, {0.50f, 0.f, 0.95f}},
|
||||
{320.f, {0.65f, 0.f, 0.80f}}, {341.f, {0.80f, 0.f, 0.60f}}, {367.f, {0.80f, 0.f, 0.55f}}};
|
||||
auto const day{std::clamp(static_cast<float>(Yearday), 1.f, 366.f)};
|
||||
for (std::size_t i = 1; i < std::size(year); ++i)
|
||||
if (day <= year[i].day)
|
||||
{
|
||||
auto const t{(day - year[i - 1].day) / (year[i].day - year[i - 1].day)};
|
||||
return glm::mix(year[i - 1].value, year[i].value, t * t * (3.f - 2.f * t));
|
||||
}
|
||||
return year[std::size(year) - 1].value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void grass_field::release(std::uint64_t const Id, chunk &Chunk)
|
||||
{
|
||||
if (Chunk.uploaded)
|
||||
gpu_release(Id);
|
||||
Chunk.uploaded = false;
|
||||
}
|
||||
|
||||
void grass_field::clear()
|
||||
{
|
||||
for (auto &entry : m_chunks)
|
||||
release(entry.first, entry.second);
|
||||
m_chunks.clear();
|
||||
}
|
||||
|
||||
void grass_field::upload(std::uint64_t const Id, chunk &Chunk, gfx::grass_chunk const &Source)
|
||||
{
|
||||
release(Id, Chunk);
|
||||
Chunk.version = Source.version;
|
||||
Chunk.corner = Source.corner;
|
||||
Chunk.size = Source.size;
|
||||
Chunk.cells = Source.cells;
|
||||
Chunk.samples = Source.samples;
|
||||
Chunk.kinds = Source.kinds;
|
||||
gpu_upload(Id, Source);
|
||||
Chunk.uploaded = true;
|
||||
|
||||
auto const side{Source.cells + 1};
|
||||
auto const samples{Source.samples + 1};
|
||||
auto const plane{static_cast<std::size_t>(samples) * samples};
|
||||
std::size_t kinds{0};
|
||||
while (kinds < Source.kinds.size() && Source.kinds[kinds] >= 0)
|
||||
++kinds;
|
||||
|
||||
// the ground and the densest place of every kind of each tile, to skip the empty tiles and to draw as many blades as needed
|
||||
auto const spacing{static_cast<double>(Source.size) / Source.cells};
|
||||
auto const step{static_cast<double>(Source.size) / Source.samples};
|
||||
auto const tilesize{static_cast<double>(Source.size) / tiles};
|
||||
for (int tz = 0; tz < tiles; ++tz)
|
||||
for (int tx = 0; tx < tiles; ++tx)
|
||||
{
|
||||
auto &part{Chunk.parts[static_cast<std::size_t>(tz) * tiles + tx]};
|
||||
auto const range = [](double const From, double const To, double const Step, int const Last) {
|
||||
return std::make_pair(std::clamp(static_cast<int>(std::floor(From / Step)), 0, Last), std::clamp(static_cast<int>(std::ceil(To / Step)), 0, Last));
|
||||
};
|
||||
auto const [gx0, gx1]{range(tx * tilesize, (tx + 1) * tilesize, spacing, Source.cells)};
|
||||
auto const [gz0, gz1]{range(tz * tilesize, (tz + 1) * tilesize, spacing, Source.cells)};
|
||||
part.low = std::numeric_limits<float>::max();
|
||||
part.high = -std::numeric_limits<float>::max();
|
||||
for (int z = gz0; z <= gz1; ++z)
|
||||
for (int x = gx0; x <= gx1; ++x)
|
||||
{
|
||||
auto const height{Source.heights[static_cast<std::size_t>(z) * side + x]};
|
||||
part.low = std::min(part.low, height);
|
||||
part.high = std::max(part.high, height);
|
||||
}
|
||||
part.most.fill(0.f);
|
||||
auto const [sx0, sx1]{range(tx * tilesize, (tx + 1) * tilesize, step, Source.samples)};
|
||||
auto const [sz0, sz1]{range(tz * tilesize, (tz + 1) * tilesize, step, Source.samples)};
|
||||
for (std::size_t kind = 0; kind < kinds; ++kind)
|
||||
{
|
||||
std::uint8_t most{0};
|
||||
for (int z = sz0; z <= sz1; ++z)
|
||||
for (int x = sx0; x <= sx1; ++x)
|
||||
most = std::max(most, Source.density[kind * plane + static_cast<std::size_t>(z) * samples + x]);
|
||||
part.most[kind] = most / 255.f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void grass_field::update(std::vector<gfx::grass_kind> const &Kinds, std::vector<gfx::grass_chunk> const &Chunks)
|
||||
{
|
||||
m_kinds = Kinds;
|
||||
for (auto &entry : m_chunks)
|
||||
entry.second.listed = false;
|
||||
if (false == Global.gfx_grass)
|
||||
{
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
for (auto const &source : Chunks)
|
||||
{
|
||||
if (source.heights == nullptr || source.density == nullptr || source.cells <= 0 || source.samples <= 0)
|
||||
continue;
|
||||
auto &entry{m_chunks[source.id]};
|
||||
entry.listed = true;
|
||||
if (false == entry.uploaded || entry.version != source.version)
|
||||
upload(source.id, entry, source);
|
||||
}
|
||||
// what's no longer near goes
|
||||
for (auto it = m_chunks.begin(); it != m_chunks.end();)
|
||||
{
|
||||
if (it->second.listed)
|
||||
{
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
release(it->first, it->second);
|
||||
it = m_chunks.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
bool grass_field::prepare(glm::dvec3 const &Camera, std::function<bool(scene::bounding_area const &)> const &Visible)
|
||||
{
|
||||
if (false == Global.gfx_grass || m_chunks.empty() || m_kinds.empty())
|
||||
return false;
|
||||
|
||||
m_chunkdraws.clear();
|
||||
m_tiledraws.clear();
|
||||
m_triangles = 0;
|
||||
// whole up to the distance, thinning out to the fading distance
|
||||
auto const fade{std::max(5.f, Global.gfx_grass_distance)};
|
||||
auto const reach{std::max(fade + 1.f, Global.gfx_grass_fadedistance)};
|
||||
auto const thinning{std::max(3.f, fade * 0.2f)};
|
||||
auto const multiplier{std::max(0.f, Global.gfx_grass_density)};
|
||||
auto const camera{Camera};
|
||||
|
||||
m_frame.lod = glm::vec4{thinning, fade, reach, 0.f};
|
||||
auto const season{grass_season(simulation::Time.year_day())};
|
||||
m_frame.season = glm::vec4{season.x, season.y, season.z, 0.f};
|
||||
// the wind sways the blades along it, harder in the gusts
|
||||
auto const wind{glm::vec3{simulation::Environment.wind()}};
|
||||
auto const windspeed{glm::length(glm::vec2{wind.x, wind.z})};
|
||||
auto const winddirection{windspeed > 0.01f ? glm::vec2{wind.x, wind.z} / windspeed : glm::vec2{1.f, 0.f}};
|
||||
m_frame.wind = glm::vec4{winddirection.x, winddirection.y, std::clamp(windspeed / 15.f, 0.f, 1.f), static_cast<float>(std::fmod(Timer::GetTime(), 3600.0))};
|
||||
|
||||
for (auto &entry : m_chunks)
|
||||
{
|
||||
auto &chunk{entry.second};
|
||||
// the kinds of the chunk, and the share of the blades of each against its densest
|
||||
std::array<glm::vec4, 4> base{}, tip{}, shape{}, dry{};
|
||||
glm::vec4 share{0.f};
|
||||
float densest{0.f};
|
||||
for (std::size_t i = 0; i < chunk.kinds.size(); ++i)
|
||||
if (chunk.kinds[i] >= 0 && chunk.kinds[i] < static_cast<int>(m_kinds.size()))
|
||||
densest = std::max(densest, m_kinds[chunk.kinds[i]].density);
|
||||
if (densest <= 0.f)
|
||||
continue;
|
||||
for (std::size_t i = 0; i < chunk.kinds.size(); ++i)
|
||||
{
|
||||
if (chunk.kinds[i] < 0 || chunk.kinds[i] >= static_cast<int>(m_kinds.size()))
|
||||
continue;
|
||||
auto const &kind{m_kinds[chunk.kinds[i]]};
|
||||
base[i] = glm::vec4{kind.base, 0.f};
|
||||
tip[i] = glm::vec4{kind.tip, 0.f};
|
||||
shape[i] = glm::vec4{kind.height, kind.variation, kind.width, kind.bend};
|
||||
dry[i] = glm::vec4{kind.dry, kind.seasonal};
|
||||
share[i] = kind.density / densest;
|
||||
}
|
||||
auto const tallest{std::max({shape[0].x, shape[1].x, shape[2].x, shape[3].x}) * 1.5f + blade_reach};
|
||||
|
||||
auto const tilesize{chunk.size / tiles};
|
||||
bool set{false};
|
||||
for (int tz = 0; tz < tiles; ++tz)
|
||||
for (int tx = 0; tx < tiles; ++tx)
|
||||
{
|
||||
auto const &part{chunk.parts[static_cast<std::size_t>(tz) * tiles + tx]};
|
||||
auto const fill{std::min(1.f, glm::dot(glm::vec4{part.most[0], part.most[1], part.most[2], part.most[3]}, share))};
|
||||
if (fill <= 0.f)
|
||||
continue;
|
||||
// distance to the box of the tile
|
||||
glm::dvec3 const low{chunk.corner.x + tx * tilesize, part.low, chunk.corner.y + tz * tilesize};
|
||||
glm::dvec3 const high{low.x + tilesize, part.high + tallest, low.z + tilesize};
|
||||
auto const nearest{glm::clamp(camera, low, high)};
|
||||
auto const distance{static_cast<float>(glm::length(nearest - camera))};
|
||||
if (distance > reach)
|
||||
continue;
|
||||
if (false == Visible(scene::bounding_area{(low + high) * 0.5, static_cast<float>(glm::length(high - low) * 0.5)}))
|
||||
continue;
|
||||
auto const full{tilesize * tilesize * densest * multiplier * fill};
|
||||
auto const count{std::min(max_blades, static_cast<int>(std::ceil(full * lod_share(distance, thinning, fade, reach))))};
|
||||
if (count <= 0)
|
||||
continue;
|
||||
if (false == set)
|
||||
{
|
||||
// the chunk is drawn relative to its corner, the heights as they are
|
||||
set = true;
|
||||
chunk_draw draw;
|
||||
draw.id = entry.first;
|
||||
draw.offset = glm::dvec3{chunk.corner.x - camera.x, -camera.y, chunk.corner.y - camera.z};
|
||||
draw.chunk = glm::vec4{chunk.size, static_cast<float>(chunk.cells), static_cast<float>(chunk.samples), 0.f};
|
||||
draw.camera = glm::vec4{static_cast<float>(camera.x - chunk.corner.x), static_cast<float>(camera.y), static_cast<float>(camera.z - chunk.corner.y), 0.f};
|
||||
draw.base = base;
|
||||
draw.tip = tip;
|
||||
draw.shape = shape;
|
||||
draw.share = share;
|
||||
draw.dry = dry;
|
||||
draw.first = m_tiledraws.size();
|
||||
m_chunkdraws.push_back(draw);
|
||||
}
|
||||
// fewer segments further away
|
||||
auto const segments{distance < thinning * 1.5f ? 3 : distance < fade * 0.5f ? 2 : 1};
|
||||
// the number of the tile in the world, the seed of its blades
|
||||
auto const seedx{static_cast<float>(std::floor(chunk.corner.x / tilesize + 0.5) + tx)};
|
||||
auto const seedz{static_cast<float>(std::floor(chunk.corner.y / tilesize + 0.5) + tz)};
|
||||
tile_draw draw;
|
||||
draw.tile = glm::vec4{tx * tilesize, tz * tilesize, tilesize, std::max(1.f, full)};
|
||||
draw.part = glm::vec4{fill, static_cast<float>(segments), seedx, seedz};
|
||||
draw.count = count;
|
||||
draw.segments = segments;
|
||||
m_tiledraws.push_back(draw);
|
||||
++m_chunkdraws.back().count;
|
||||
m_triangles += static_cast<std::size_t>(count) * (segments * 2 - 1);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
133
rendering/common/grassfield.h
Normal file
133
rendering/common/grassfield.h
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
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 <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "rendering/renderer.h"
|
||||
#include "scene/scenenode.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
// grass of the heightmap terrain, whichever api draws it: blades made up by the vertex shader (grass.vert) where the grass is
|
||||
// painted, in the chunks near the camera.
|
||||
//
|
||||
// a chunk is cut into tiles; each tile visible within the reach of the grass is drawn as instances of a blade, as many as
|
||||
// its densest kind and its distance ask for. a blade finds its place, its kind and its shape from its number, so the same
|
||||
// blades stand at the same places in every frame. further away the blades thin out and grow wider, and fade out at the end
|
||||
// of the reach. the heights, the densities and the holes of a chunk are kept in textures, made by the renderer (the gpu_
|
||||
// functions) when the chunk comes near; what is drawn in a frame is worked out here (prepare())
|
||||
class grass_field
|
||||
{
|
||||
public:
|
||||
virtual ~grass_field() = default;
|
||||
|
||||
// see gfx_renderer::Terrain_Grass()
|
||||
void update(std::vector<gfx::grass_kind> const &Kinds, std::vector<gfx::grass_chunk> const &Chunks);
|
||||
// drops everything made on the gpu, e.g. when the context goes
|
||||
void clear();
|
||||
bool empty() const
|
||||
{
|
||||
return m_chunks.empty();
|
||||
}
|
||||
|
||||
static int constexpr tiles{16}; // along a side of a chunk
|
||||
|
||||
// what a frame draws (the uniforms of grass.vert): the same for all draws
|
||||
struct frame_draw
|
||||
{
|
||||
glm::vec4 lod{0.f}; // grass_lod
|
||||
glm::vec4 season{0.f}; // grass_season
|
||||
glm::vec4 wind{0.f}; // grass_wind
|
||||
};
|
||||
// a chunk with tiles seen, drawn relative to its corner
|
||||
struct chunk_draw
|
||||
{
|
||||
std::uint64_t id{0}; // of the chunk, as the renderer got it in gpu_upload()
|
||||
glm::dvec3 offset{0.0}; // of the corner from the camera, the heights as they are
|
||||
glm::vec4 chunk{0.f}; // grass_chunk
|
||||
glm::vec4 camera{0.f}; // grass_camera
|
||||
std::array<glm::vec4, 4> base{}, tip{}, shape{}, dry{};
|
||||
glm::vec4 share{0.f};
|
||||
std::size_t first{0}; // tiles drawn, in tile_draws()
|
||||
std::size_t count{0};
|
||||
};
|
||||
// a tile drawn: Count instances of a strip of 2 * Segments + 1 vertices
|
||||
struct tile_draw
|
||||
{
|
||||
glm::vec4 tile{0.f}; // grass_tile
|
||||
glm::vec4 part{0.f}; // grass_part
|
||||
int count{0};
|
||||
int segments{0};
|
||||
};
|
||||
// the draws of the grass seen from Camera: false when there is nothing to draw (grass off, no chunks or kinds), the draws of
|
||||
// the last call are left then. Visible: whether the bounds of a tile are seen
|
||||
bool prepare(glm::dvec3 const &Camera, std::function<bool(scene::bounding_area const &)> const &Visible);
|
||||
frame_draw const &frame() const
|
||||
{
|
||||
return m_frame;
|
||||
}
|
||||
std::vector<chunk_draw> const &chunk_draws() const
|
||||
{
|
||||
return m_chunkdraws;
|
||||
}
|
||||
std::vector<tile_draw> const &tile_draws() const
|
||||
{
|
||||
return m_tiledraws;
|
||||
}
|
||||
// the triangles of the draws prepared
|
||||
std::size_t triangles() const
|
||||
{
|
||||
return m_triangles;
|
||||
}
|
||||
|
||||
protected:
|
||||
// the textures of the chunk Id made of Source: heights (R32F, the grid points, cells + 1 along a side), densities (RGBA8, the
|
||||
// densities of the kinds side by side, samples + 1 along a side), holes (R8, a single texel of 0 without holes)
|
||||
virtual void gpu_upload(std::uint64_t const Id, gfx::grass_chunk const &Source) = 0;
|
||||
// the textures of the chunk go; nothing when it has none
|
||||
virtual void gpu_release(std::uint64_t const Id) = 0;
|
||||
|
||||
private:
|
||||
struct tile
|
||||
{
|
||||
float low{0.f}, high{0.f}; // of the ground
|
||||
std::array<float, 4> most{}; // the highest density of each kind of the chunk within the tile, 0..1
|
||||
};
|
||||
struct chunk
|
||||
{
|
||||
std::uint64_t version{0};
|
||||
glm::dvec2 corner{0.0};
|
||||
float size{0.f};
|
||||
int cells{0};
|
||||
int samples{0};
|
||||
std::array<int, 4> kinds{-1, -1, -1, -1};
|
||||
std::array<tile, tiles * tiles> parts;
|
||||
bool uploaded{false}; // has its textures
|
||||
bool listed{false}; // in the last list given
|
||||
};
|
||||
|
||||
void upload(std::uint64_t const Id, chunk &Chunk, gfx::grass_chunk const &Source);
|
||||
void release(std::uint64_t const Id, chunk &Chunk);
|
||||
|
||||
std::vector<gfx::grass_kind> m_kinds;
|
||||
std::unordered_map<std::uint64_t, chunk> m_chunks;
|
||||
frame_draw m_frame;
|
||||
std::vector<chunk_draw> m_chunkdraws;
|
||||
std::vector<tile_draw> m_tiledraws;
|
||||
std::size_t m_triangles{0};
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
389
rendering/common/instancecull.cpp
Normal file
389
rendering/common/instancecull.cpp
Normal file
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/instancecull.h"
|
||||
|
||||
#include "model/AnimModel.h"
|
||||
#include "model/Model3d.h"
|
||||
#include "rendering/frustum.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/utilities.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr std::int64_t RECORD_SIZE{sizeof(scene::basic_cell::instance_record)};
|
||||
|
||||
} // namespace
|
||||
|
||||
instance_cull::instance_cull() : m_records{std::make_shared<record_state>()}
|
||||
{
|
||||
m_readbackdata.resize(gl::INSTANCE_COUNTER_HEADER + READBACK_ENTRIES);
|
||||
}
|
||||
|
||||
void instance_cull::begin_pass(pass_setup const &Setup)
|
||||
{
|
||||
collect();
|
||||
|
||||
m_inpass = true;
|
||||
m_ran = true;
|
||||
m_dispatched = false;
|
||||
m_setup = Setup;
|
||||
m_buckets.clear();
|
||||
m_joblist.clear();
|
||||
m_grouplist.clear();
|
||||
m_drawgroups.clear();
|
||||
m_lodlist.clear();
|
||||
m_groupmap.clear();
|
||||
m_sleepergroupmap.clear();
|
||||
m_cpucounts.clear();
|
||||
m_candidates = 0;
|
||||
m_entries = 0;
|
||||
m_sleepertracks = 0;
|
||||
m_sleepers = 0;
|
||||
m_models = 0;
|
||||
m_counts = {};
|
||||
}
|
||||
|
||||
void instance_cull::add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area)
|
||||
{
|
||||
add(Key, Bucket, Area, false);
|
||||
}
|
||||
|
||||
void instance_cull::skip_bucket(scene::basic_cell::instance_bucket const &Bucket)
|
||||
{
|
||||
if (!m_inpass)
|
||||
return;
|
||||
++m_counts.skippedbuckets;
|
||||
m_counts.skippedinstances += Bucket.records.size();
|
||||
}
|
||||
|
||||
void instance_cull::add_sleepers(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area)
|
||||
{
|
||||
add(Key, Bucket, Area, true);
|
||||
}
|
||||
|
||||
void instance_cull::add(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area, bool const Sleepers)
|
||||
{
|
||||
if (!m_inpass || Bucket.records.empty() || Key.pModel == nullptr)
|
||||
return;
|
||||
|
||||
// range of the squared distances the instances of the bucket can be at, scaled as in the culling, with some slack for
|
||||
// its single precision: the draws are made only for the lod bands within it. sleepers go by the plain distance from the
|
||||
// camera of the pass
|
||||
auto const &distancecamera{Sleepers ? m_setup.camera : m_setup.distancecamera};
|
||||
auto const distancescale{Sleepers ? 1.0 : static_cast<double>(m_setup.distancescale)};
|
||||
auto const distanceoffset{Sleepers ? 0.0 : static_cast<double>(m_setup.distanceoffset)};
|
||||
auto const centerdistance{glm::length(Area.center - distancecamera)};
|
||||
auto const nearest{std::max(0.0, centerdistance - Area.radius)};
|
||||
auto const farthest{centerdistance + Area.radius};
|
||||
auto const distancemin{static_cast<float>(0.999 * (nearest * nearest * distancescale + distanceoffset))};
|
||||
auto const distancemax{static_cast<float>(1.001 * (farthest * farthest * distancescale + distanceoffset))};
|
||||
|
||||
auto &groupmap{Sleepers ? m_sleepergroupmap : m_groupmap};
|
||||
auto lookup{groupmap.find(Key)};
|
||||
if (lookup == groupmap.end())
|
||||
{
|
||||
// lod bounds of the model, the same set Render_Instanced() splits the distance into bands with, found once for the model
|
||||
auto const lodfirst{static_cast<std::uint32_t>(m_lodlist.size())};
|
||||
auto const &lodbounds{Key.pModel->lod_bounds()};
|
||||
m_lodlist.insert(m_lodlist.end(), lodbounds.begin(), lodbounds.end());
|
||||
auto const lodcount{static_cast<std::uint32_t>(lodbounds.size())};
|
||||
|
||||
group_data const group{static_cast<std::uint32_t>(m_grouplist.size()), m_entries, lodcount + 1, distancemin, distancemax};
|
||||
m_grouplist.push_back({lodfirst, lodcount, m_entries, 0});
|
||||
m_entries += group.entrycount;
|
||||
lookup = groupmap.emplace(Key, group).first;
|
||||
if (m_setup.drawing)
|
||||
{
|
||||
// any instance of the group serves; it's only prepared and asked for its material
|
||||
TAnimModel *instance{nullptr};
|
||||
for (auto *candidate : Bucket.instances)
|
||||
{
|
||||
if (candidate != nullptr)
|
||||
{
|
||||
instance = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_drawgroups.push_back({&lookup->first, instance, Sleepers, lodfirst, lodcount, group.entryfirst, distancemin, distancemax});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lookup->second.distancemin = std::min(lookup->second.distancemin, distancemin);
|
||||
lookup->second.distancemax = std::max(lookup->second.distancemax, distancemax);
|
||||
if (m_setup.drawing)
|
||||
{
|
||||
auto &drawgroup{m_drawgroups[lookup->second.index]};
|
||||
drawgroup.distancemin = lookup->second.distancemin;
|
||||
drawgroup.distancemax = lookup->second.distancemax;
|
||||
if (drawgroup.instance == nullptr)
|
||||
{
|
||||
for (auto *candidate : Bucket.instances)
|
||||
{
|
||||
if (candidate != nullptr)
|
||||
{
|
||||
drawgroup.instance = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
place(Bucket);
|
||||
|
||||
gl::instance_cull_job job{};
|
||||
job.origin = packed_vec3{Bucket.recordorigin - m_setup.camera};
|
||||
job.first = Bucket.gpufirst;
|
||||
job.distanceorigin = packed_vec3{Bucket.recordorigin - m_setup.distancecamera};
|
||||
job.count = static_cast<std::uint32_t>(Bucket.records.size());
|
||||
job.group = lookup->second.index;
|
||||
job.candidatestart = m_candidates;
|
||||
job.flags = (Sleepers ? gl::INSTANCE_JOB_SLEEPERS : 0u);
|
||||
m_joblist.push_back(job);
|
||||
m_buckets.push_back(&Bucket);
|
||||
m_candidates += job.count;
|
||||
if (Sleepers)
|
||||
{
|
||||
++m_sleepertracks;
|
||||
m_sleepers += job.count;
|
||||
}
|
||||
else if (Key.instance != nullptr)
|
||||
{
|
||||
++m_models;
|
||||
}
|
||||
}
|
||||
|
||||
void instance_cull::place(scene::basic_cell::instance_bucket const &Bucket)
|
||||
{
|
||||
auto &shared{*m_records};
|
||||
auto const count{static_cast<std::uint32_t>(Bucket.records.size())};
|
||||
auto const current{Bucket.gpugeneration == shared.recordgeneration};
|
||||
if (current && !Bucket.gpustale)
|
||||
return;
|
||||
if (current && count <= Bucket.gpucapacity)
|
||||
{
|
||||
send_records(Bucket.gpufirst, Bucket.records.data(), count);
|
||||
m_counts.sentrecords += count;
|
||||
Bucket.gpustale = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// a new place, with some room for instances the editor adds
|
||||
auto const allocate = [this, &shared](scene::basic_cell::instance_bucket const &Target) {
|
||||
auto const records{static_cast<std::uint32_t>(Target.records.size())};
|
||||
Target.gpufirst = shared.recordfill;
|
||||
Target.gpucapacity = records + records / 4;
|
||||
Target.gpugeneration = shared.recordgeneration;
|
||||
Target.gpustale = false;
|
||||
shared.recordfill += Target.gpucapacity;
|
||||
send_records(Target.gpufirst, Target.records.data(), records);
|
||||
m_counts.sentrecords += records;
|
||||
};
|
||||
|
||||
auto const capacity{static_cast<std::uint32_t>(record_buffer_size() / RECORD_SIZE)};
|
||||
if (shared.recordfill + count + count / 4 > capacity)
|
||||
{
|
||||
// the buffer is started over, and only the buckets of this pass get places again. the places left behind by
|
||||
// buckets which were made anew or are out of sight are dropped this way. the passes of a frame queue different
|
||||
// buckets: when those of all of them don't fit in the buffer together, some pass would start it over in every frame,
|
||||
// so a buffer started over again in the same or the next frame grows
|
||||
std::int64_t live{count + count / 4};
|
||||
for (auto const *bucket : m_buckets)
|
||||
live += bucket->records.size() + bucket->records.size() / 4;
|
||||
auto const again{shared.restarted && (m_setup.frame - shared.restartframe <= 1)};
|
||||
if ((2 * live * RECORD_SIZE > record_buffer_size()) || again)
|
||||
resize_record_buffer(std::max(2 * live * RECORD_SIZE, 2 * record_buffer_size()));
|
||||
else
|
||||
restart_records();
|
||||
shared.restarted = true;
|
||||
shared.restartframe = m_setup.frame;
|
||||
m_counts.restarted = true;
|
||||
++shared.recordgeneration;
|
||||
shared.recordfill = 0;
|
||||
for (std::size_t job = 0; job < m_buckets.size(); ++job)
|
||||
{
|
||||
allocate(*m_buckets[job]);
|
||||
m_joblist[job].first = m_buckets[job]->gpufirst;
|
||||
}
|
||||
}
|
||||
allocate(Bucket);
|
||||
}
|
||||
|
||||
bool instance_cull::dispatch_begin()
|
||||
{
|
||||
m_occlusion = false;
|
||||
m_twophase = false;
|
||||
return (m_inpass && !m_joblist.empty());
|
||||
}
|
||||
|
||||
void instance_cull::frustum_planes(glm::vec4 (&Planes)[6]) const
|
||||
{
|
||||
auto const &frustumplanes{m_setup.frustum->planes()};
|
||||
for (int plane = 0; plane < 6; ++plane)
|
||||
{
|
||||
auto const *source{frustumplanes[plane]};
|
||||
Planes[plane] = glm::vec4{source[0], source[1], source[2],
|
||||
static_cast<float>(source[0] * m_setup.camera.x + source[1] * m_setup.camera.y + source[2] * m_setup.camera.z + source[3])};
|
||||
}
|
||||
}
|
||||
|
||||
void instance_cull::dispatch_end()
|
||||
{
|
||||
m_cpucounts.assign(m_entries, 0);
|
||||
m_dispatched = true;
|
||||
}
|
||||
|
||||
void instance_cull::draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, std::uint32_t const Calls, std::uint32_t const Prepasscalls, double const Cputime)
|
||||
{
|
||||
m_draws = Draws;
|
||||
m_prepassdraws = Prepassdraws;
|
||||
m_calls = Calls;
|
||||
m_prepasscalls = Prepasscalls;
|
||||
m_cputime = Cputime;
|
||||
}
|
||||
|
||||
void instance_cull::cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector<std::uint32_t> const &Bandcounts, bool const Sleepers)
|
||||
{
|
||||
if (!m_dispatched)
|
||||
return;
|
||||
auto const &groupmap{Sleepers ? m_sleepergroupmap : m_groupmap};
|
||||
auto const lookup{groupmap.find(Key)};
|
||||
if (lookup == groupmap.end())
|
||||
return;
|
||||
auto const &group{lookup->second};
|
||||
for (std::size_t band = 0; band < std::min<std::size_t>(Bandcounts.size(), group.entrycount); ++band)
|
||||
m_cpucounts[group.entryfirst + band] += Bandcounts[band];
|
||||
}
|
||||
|
||||
void instance_cull::cpu_instance(scene::basic_cell::instance_bucket_key const &Key, float const Distancesquared)
|
||||
{
|
||||
if (!m_dispatched)
|
||||
return;
|
||||
auto const lookup{m_groupmap.find(Key)};
|
||||
if (lookup == m_groupmap.end())
|
||||
return;
|
||||
auto const &group{lookup->second};
|
||||
auto const &lods{m_grouplist[group.index]};
|
||||
// the band the culling gives the distance: the number of bounds it reached
|
||||
auto const first{m_lodlist.begin() + lods.lodfirst};
|
||||
auto const band{static_cast<std::uint32_t>(std::upper_bound(first, first + lods.lodcount, Distancesquared) - first)};
|
||||
if (band < group.entrycount)
|
||||
++m_cpucounts[group.entryfirst + band];
|
||||
}
|
||||
|
||||
void instance_cull::end_pass()
|
||||
{
|
||||
// the counts of both phases, for the panel and the comparison
|
||||
if (m_dispatched)
|
||||
push_counts();
|
||||
// the cpu counts are there to compare with only when the cpu drew
|
||||
if (m_dispatched && !m_setup.drawing)
|
||||
{
|
||||
std::uint64_t total{0};
|
||||
for (auto const count : m_cpucounts)
|
||||
total += count;
|
||||
m_cpupasses.push_back({m_setup.frame, m_cpucounts, total});
|
||||
// the gpu is never this far behind
|
||||
while (m_cpupasses.size() > 8)
|
||||
m_cpupasses.pop_front();
|
||||
}
|
||||
m_lastsleepertracks = m_sleepertracks;
|
||||
m_lastsleepers = m_sleepers;
|
||||
m_lastmodels = m_models;
|
||||
m_counts.jobs = static_cast<std::uint32_t>(m_joblist.size());
|
||||
m_counts.candidates = m_candidates;
|
||||
m_lastcounts = m_counts;
|
||||
m_inpass = false;
|
||||
m_dispatched = false;
|
||||
}
|
||||
|
||||
void instance_cull::collect()
|
||||
{
|
||||
if (!pop_counts(m_readbackdata.data()))
|
||||
return;
|
||||
|
||||
auto const frame{m_readbackdata[0]};
|
||||
comparison result;
|
||||
result.frame = frame;
|
||||
result.occluded = m_readbackdata[3];
|
||||
result.gputotal = m_readbackdata[1] - std::min(m_readbackdata[1], result.occluded);
|
||||
result.entries = m_readbackdata[2];
|
||||
result.first = m_readbackdata[4];
|
||||
while (!m_cpupasses.empty() && m_cpupasses.front().frame != frame)
|
||||
m_cpupasses.pop_front();
|
||||
if (m_cpupasses.empty())
|
||||
{
|
||||
// drawn from the gpu: nothing to compare with
|
||||
m_last = result;
|
||||
return;
|
||||
}
|
||||
|
||||
auto const &cpu{m_cpupasses.front()};
|
||||
result.cputotal = cpu.total;
|
||||
result.compared = std::min<std::uint32_t>({result.entries, READBACK_ENTRIES, static_cast<std::uint32_t>(cpu.counts.size())});
|
||||
for (std::uint32_t entry = 0; entry < result.compared; ++entry)
|
||||
{
|
||||
auto const gpu{m_readbackdata[gl::INSTANCE_COUNTER_HEADER + entry]};
|
||||
auto const difference{gpu > cpu.counts[entry] ? gpu - cpu.counts[entry] : cpu.counts[entry] - gpu};
|
||||
if (difference > 0)
|
||||
{
|
||||
++result.mismatched;
|
||||
result.largestdifference = std::max(result.largestdifference, difference);
|
||||
}
|
||||
}
|
||||
m_cpupasses.pop_front();
|
||||
|
||||
m_last = result;
|
||||
++m_comparedframes;
|
||||
if ((result.mismatched > 0) || (result.gputotal != result.cputotal) || (result.entries != cpu.counts.size()))
|
||||
++m_mismatchedframes;
|
||||
}
|
||||
|
||||
std::string instance_cull::info(std::string const &Label, bool const Occlusion)
|
||||
{
|
||||
if (Global.gfx_gpucull == 0)
|
||||
return {};
|
||||
// the sleepers are among the instances counted
|
||||
auto sleepers{m_lastsleepertracks > 0 ? "\n" + Label + ": " + std::to_string(m_lastsleepers) + " sleepers of " + std::to_string(m_lastsleepertracks) + " tracks queued" : std::string{}};
|
||||
// and so are the models drawn on their own
|
||||
if (m_lastmodels > 0)
|
||||
sleepers += "\n" + Label + ": " + std::to_string(m_lastmodels) + " models drawn on their own queued";
|
||||
// what the pass tested and sent to the gpu
|
||||
sleepers += "\n" + Label + ": " + std::to_string(m_lastcounts.candidates) + " instances tested in " + std::to_string(m_lastcounts.jobs) + " buckets";
|
||||
if (Global.gfx_gpucull_bucketrange)
|
||||
sleepers += ", " + std::to_string(m_lastcounts.skippedbuckets) + " buckets (" + std::to_string(m_lastcounts.skippedinstances) + " instances) left out before the culling";
|
||||
sleepers += ", " + std::to_string(m_lastcounts.sentrecords) + " records sent" + (m_lastcounts.restarted ? ", record buffer started over" : "");
|
||||
if (Global.gfx_gpucull == 1)
|
||||
{
|
||||
if (m_last.frame == 0)
|
||||
return "\n" + Label + ": no result yet" + sleepers;
|
||||
if (false == Occlusion)
|
||||
return "\n" + Label + ": " + std::to_string(m_last.gputotal) + " instances drawn, " + std::to_string(m_last.entries) + " bands; " + std::to_string(m_draws) +
|
||||
" indirect draws in " + std::to_string(m_calls) + " calls, cpu " + to_string(m_cputime, 3) + " ms" + sleepers;
|
||||
return "\n" + Label + ": " + std::to_string(m_last.gputotal) + " instances drawn" +
|
||||
(m_twophase ? " (" + std::to_string(m_last.first) + " of them first, seen in the last frame)" : std::string{}) + ", " + std::to_string(m_last.occluded) +
|
||||
" hidden by occlusion" + (m_occlusion ? (m_twophase ? " (also by one another)" : "") : " (test off)") + ", " + std::to_string(m_last.entries) + " bands; " +
|
||||
std::to_string(m_draws) + " indirect draws in " + std::to_string(m_calls) + " calls (+" + std::to_string(m_prepassdraws) + " depth prepass in " +
|
||||
std::to_string(m_prepasscalls) + "), cpu " + to_string(m_cputime, 3) + " ms" + sleepers;
|
||||
}
|
||||
if (m_comparedframes == 0)
|
||||
return "\n" + Label + ": no result yet" + sleepers;
|
||||
return "\n" + Label + ": " + std::to_string(m_last.gputotal) + " instances (cpu " + std::to_string(m_last.cputotal) + "), " + std::to_string(m_last.entries) + " bands, " +
|
||||
std::to_string(m_last.mismatched) + " differ (by up to " + std::to_string(m_last.largestdifference) + "); frames differing " + std::to_string(m_mismatchedframes) + " of " +
|
||||
std::to_string(m_comparedframes) + sleepers;
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
303
rendering/common/instancecull.h
Normal file
303
rendering/common/instancecull.h
Normal file
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
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 <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "gl/ubo.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
class cFrustum;
|
||||
class TAnimModel;
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
// the cpu side of the culling of the model instances of a render pass in compute passes, shared by the renderers (stage N6c of
|
||||
// claude/vulkan14-renderer-plan.md); the backend (opengl46_instance_cull, vk14 instance_cull) keeps the buffers and runs the passes:
|
||||
// - the records of the instances (scene::basic_cell::instance_record) are kept in one buffer, each bucket of a scene cell
|
||||
// in its own place, and sent again only when the bucket made them anew
|
||||
// - every bucket queued in the pass becomes a job; the buckets of one model and one set of skins make a group, whose
|
||||
// lod bands are its entries
|
||||
// - the passes count the instances which pass per entry, turn the counts into places in the output and write the
|
||||
// modelview matrices of the instances there
|
||||
// with Global.gfx_gpucull 1 the instances are drawn from the result: the renderer turns each lod band of a group into a
|
||||
// list of indirect draw commands (one per submodel drawn in the band), whose instances a fourth pass fills in from the
|
||||
// counts (instance_cull_commands.comp), and draws them with the output bound as the instance buffer. an occlusion test
|
||||
// against a depth pyramid of what the pass drew before can leave out more instances. in two phases the instances seen in the
|
||||
// previous frame are culled and drawn first, then the pyramid is made again with them in it and the others are tested
|
||||
// against it, so the instances hide one another too. the command buffer holds the commands three times over: the instances
|
||||
// drawn first, all of them, and the ones drawn second, each copy command_count() commands long (draw_command_offset())
|
||||
// with Global.gfx_gpucull 2 the cpu still draws, and the result is only compared with its own: the counts per entry are
|
||||
// read back a few frames later and set against the counts Render_Instanced() reported for the same frame
|
||||
// each render pass culled on the gpu (the main view, the cascades of the sun shadows, the faces of the environment map, the
|
||||
// shadow maps of headlights and the scenery parts of those of lamps) has a culling of its own, with its own buffers, while the
|
||||
// records and the programs are shared by all of them: the records of a bucket go to the gpu once and serve every pass which
|
||||
// queues the bucket
|
||||
// the sleepers of tracks (TTrack::m_sleeper_local_transforms) are culled the same way, each track with them a bucket of its
|
||||
// own, and drawn together with the instances; their groups are kept apart from those of the model instances
|
||||
// so are the model instances drawn on their own (not instanceable): each one a bucket and a group of its own (the instance in
|
||||
// its key), whose submodel tree the renderer animates and walks for it alone
|
||||
class instance_cull
|
||||
{
|
||||
public:
|
||||
// what a pass needs for the tests, the way Render_Instanced() takes it
|
||||
struct pass_setup
|
||||
{
|
||||
unsigned int frame{0};
|
||||
glm::dvec3 camera{0.0}; // camera of the pass: frustum and placement
|
||||
glm::dvec3 distancecamera{0.0}; // the distance is measured from it
|
||||
cFrustum const *frustum{nullptr}; // of the pass camera, in world coordinates
|
||||
float distancescale{1.f}; // of the squared distance
|
||||
float distanceoffset{0.f}; // added to the scaled squared distance past the range test
|
||||
float distancerange{0.f}; // squared range of the scaled distance before the offset
|
||||
float drawrange{0.f}; // squared draw range
|
||||
float zoom{1.f};
|
||||
bool drawing{false}; // the instances are drawn from the result (Global.gfx_gpucull 1), not compared
|
||||
};
|
||||
// a group of instances (model and skins) of the pass, for the renderer to make the draw commands of
|
||||
struct draw_group
|
||||
{
|
||||
scene::basic_cell::instance_bucket_key const *key;
|
||||
TAnimModel *instance; // one of the group, prepares the model and gives the material. none for sleepers
|
||||
bool sleepers; // the skins come from the key, as Render_Sleepers() sets them
|
||||
std::uint32_t lodfirst; // in lod_bounds()
|
||||
std::uint32_t lodcount;
|
||||
std::uint32_t entryfirst;
|
||||
float distancemin; // squared distances (scaled as the culling does) the instances of the group can be at
|
||||
float distancemax;
|
||||
};
|
||||
// copies of the commands in the command buffer
|
||||
enum class command_set : std::uint32_t
|
||||
{
|
||||
first = 0, // instances which passed the first dispatch: in one phase all of them
|
||||
all = 1, // after the second phase
|
||||
second = 2 // the ones which passed the second phase
|
||||
};
|
||||
|
||||
instance_cull();
|
||||
instance_cull(const instance_cull &) = delete;
|
||||
instance_cull &operator=(const instance_cull &) = delete;
|
||||
virtual ~instance_cull() = default;
|
||||
|
||||
// starts collecting the buckets of a pass
|
||||
void begin_pass(pass_setup const &Setup);
|
||||
bool in_pass() const
|
||||
{
|
||||
return m_inpass;
|
||||
}
|
||||
bool drawing() const
|
||||
{
|
||||
return m_inpass && m_setup.drawing;
|
||||
}
|
||||
// adds the records of a bucket (made already) to the pass, sending them to the gpu when they changed.
|
||||
// Area: bounds which hold the locations of all its visible instances: their own (scene_walk::Bucket_Area()) or of the cell
|
||||
// holding the bucket
|
||||
void add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area);
|
||||
// adds the sleepers of a track, their records made already as a bucket with no instances (only the records and the place
|
||||
// in the record buffer count). Area: bounds of the locations of the sleepers. the range of the whole track is tested by
|
||||
// the caller, the culling tests each sleeper against the frustum and gives it the lod band of its plain distance from the
|
||||
// camera of the pass (Render_Sleepers())
|
||||
void add_sleepers(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area);
|
||||
// a bucket the renderer left out of the pass, as none of its instances can pass the culling (Global.gfx_gpucull_bucketrange): only counted,
|
||||
// for the panel
|
||||
void skip_bucket(scene::basic_cell::instance_bucket const &Bucket);
|
||||
// the last dispatch works in two phases
|
||||
bool two_phase() const
|
||||
{
|
||||
return m_twophase;
|
||||
}
|
||||
// groups of the pass and the lod bounds they refer to, valid from the dispatch to end_pass()
|
||||
std::vector<draw_group> const &draw_groups() const
|
||||
{
|
||||
return m_drawgroups;
|
||||
}
|
||||
std::vector<float> const &lod_bounds() const
|
||||
{
|
||||
return m_lodlist;
|
||||
}
|
||||
// offset of command Index of a copy in the command buffer
|
||||
std::intptr_t draw_command_offset(command_set const Set, std::size_t const Index) const
|
||||
{
|
||||
return static_cast<std::intptr_t>((static_cast<std::size_t>(Set) * m_commandcount + Index) * sizeof(gl::instance_draw_command));
|
||||
}
|
||||
// counts of the last result which came back from the gpu, with the draws of the pass and the calls which made them (fewer
|
||||
// with multi draws), for the debug panel
|
||||
void draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, std::uint32_t const Calls, std::uint32_t const Prepasscalls, double const Cputime);
|
||||
// what draw_result() was given last
|
||||
std::uint32_t prepass_draws() const
|
||||
{
|
||||
return m_prepassdraws;
|
||||
}
|
||||
std::uint32_t prepass_calls() const
|
||||
{
|
||||
return m_prepasscalls;
|
||||
}
|
||||
double cpu_time() const
|
||||
{
|
||||
return m_cputime;
|
||||
}
|
||||
// instances which passed in the last result which came back from the gpu, a few frames old
|
||||
std::uint32_t last_count() const
|
||||
{
|
||||
return static_cast<std::uint32_t>(m_last.gputotal);
|
||||
}
|
||||
// counts per lod band of the instances Render_Instanced() drew for a model and its skins in this pass, or of the sleepers
|
||||
// Render_Sleepers() drew. needs the dispatch before it
|
||||
void cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector<std::uint32_t> const &Bandcounts, bool const Sleepers = false);
|
||||
// the cpu drew the instance of a group of its own (Key with the instance) at a squared distance scaled as the culling
|
||||
// scales it: counted in the lod band the distance falls in. needs the dispatch before it
|
||||
void cpu_instance(scene::basic_cell::instance_bucket_key const &Key, float const Distancesquared);
|
||||
// ends the pass: its cpu counts are kept for the comparison
|
||||
void end_pass();
|
||||
// takes the newest counts of the gpu which arrived and compares them with the ones of the cpu. done when a pass begins;
|
||||
// for the panel also by the culls of passes which aren't made every frame
|
||||
void collect();
|
||||
// a pass was culled since the start
|
||||
bool ran() const
|
||||
{
|
||||
return m_ran;
|
||||
}
|
||||
// lines for the debug panel, Label naming the pass. Occlusion: the pass may test occlusion (the main view)
|
||||
std::string info(std::string const &Label = "gpu cull", bool const Occlusion = true);
|
||||
|
||||
protected:
|
||||
// what the cullings of all passes share of the record buffer: records placed so far, the generation which tells current places
|
||||
// from the ones of a replaced buffer, the frame of the last time the buffer was started over, and whether it was
|
||||
struct record_state
|
||||
{
|
||||
std::uint32_t recordfill{0};
|
||||
unsigned int recordgeneration{1};
|
||||
unsigned int restartframe{0};
|
||||
bool restarted{false};
|
||||
};
|
||||
// entries read back for the comparison; with more of them only the totals and these are compared
|
||||
static constexpr std::uint32_t READBACK_ENTRIES{16384};
|
||||
|
||||
// what the backend does with the buffers
|
||||
// size of the record buffer, bytes, and a new one of the given size (the contents are dropped)
|
||||
virtual std::int64_t record_buffer_size() const = 0;
|
||||
virtual void resize_record_buffer(std::int64_t const Size) = 0;
|
||||
// sends records to their place in the record buffer
|
||||
virtual void send_records(std::uint32_t const First, scene::basic_cell::instance_record const *Records, std::uint32_t const Count) = 0;
|
||||
// the buffer is started over without growing: the records go to the places of the buffer from its start again
|
||||
virtual void restart_records() {}
|
||||
// the counters of the pass (INSTANCE_COUNTER_HEADER + READBACK_ENTRIES words) go back to the cpu, and the oldest of them which
|
||||
// arrived: false when none did
|
||||
virtual void push_counts() = 0;
|
||||
virtual bool pop_counts(std::uint32_t *Counts) = 0;
|
||||
|
||||
// the start of the dispatch of the first phase: false when there's nothing to cull
|
||||
bool dispatch_begin();
|
||||
// planes of the frustum of the pass moved to its camera, in double precision: the cpu tests world coordinates
|
||||
void frustum_planes(glm::vec4 (&Planes)[6]) const;
|
||||
// the end of the dispatch of the first phase
|
||||
void dispatch_end();
|
||||
// with a culling made for another pass: shares the records with it
|
||||
void share_records(instance_cull const &Shared)
|
||||
{
|
||||
m_records = Shared.m_records;
|
||||
}
|
||||
|
||||
struct group_data
|
||||
{
|
||||
std::uint32_t index;
|
||||
std::uint32_t entryfirst;
|
||||
std::uint32_t entrycount;
|
||||
float distancemin;
|
||||
float distancemax;
|
||||
};
|
||||
// counts of the cpu of a pass, waiting for those of the gpu
|
||||
struct cpu_pass
|
||||
{
|
||||
unsigned int frame;
|
||||
std::vector<std::uint32_t> counts;
|
||||
std::uint64_t total;
|
||||
};
|
||||
struct comparison
|
||||
{
|
||||
unsigned int frame{0};
|
||||
std::uint64_t gputotal{0};
|
||||
std::uint64_t cputotal{0};
|
||||
std::uint32_t entries{0};
|
||||
std::uint32_t compared{0}; // entries compared, at most READBACK_ENTRIES
|
||||
std::uint32_t mismatched{0};
|
||||
std::uint32_t largestdifference{0};
|
||||
std::uint32_t occluded{0}; // left out by the occlusion test
|
||||
std::uint32_t first{0}; // drawn first, seen in the previous frame (two phases)
|
||||
};
|
||||
|
||||
using group_map = std::unordered_map<scene::basic_cell::instance_bucket_key, group_data, scene::basic_cell::instance_bucket_key_hash>;
|
||||
|
||||
// add_bucket() and add_sleepers()
|
||||
void add(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area, bool const Sleepers);
|
||||
// finds a place for the records of a bucket in the record buffer and sends them
|
||||
void place(scene::basic_cell::instance_bucket const &Bucket);
|
||||
|
||||
std::shared_ptr<record_state> m_records;
|
||||
|
||||
bool m_inpass{false};
|
||||
bool m_ran{false};
|
||||
pass_setup m_setup;
|
||||
std::vector<scene::basic_cell::instance_bucket const *> m_buckets; // of the jobs, in their order
|
||||
std::vector<gl::instance_cull_job> m_joblist;
|
||||
std::vector<gl::instance_cull_group> m_grouplist;
|
||||
std::vector<draw_group> m_drawgroups;
|
||||
std::vector<float> m_lodlist;
|
||||
group_map m_groupmap;
|
||||
group_map m_sleepergroupmap; // groups of the sleepers
|
||||
std::uint32_t m_candidates{0};
|
||||
std::uint32_t m_entries{0};
|
||||
// tracks with sleepers and their sleepers queued in the pass, and in the last pass, for the panel
|
||||
std::uint32_t m_sleepertracks{0};
|
||||
std::uint32_t m_sleepers{0};
|
||||
std::uint32_t m_lastsleepertracks{0};
|
||||
std::uint32_t m_lastsleepers{0};
|
||||
// model instances drawn on their own queued in the pass, and in the last pass, for the panel
|
||||
std::uint32_t m_models{0};
|
||||
std::uint32_t m_lastmodels{0};
|
||||
// what the pass held and sent, and the same of the last pass, for the panel: jobs (buckets queued), buckets left out beyond
|
||||
// their range and their instances, records sent to the gpu, the record buffer started over
|
||||
struct pass_counts
|
||||
{
|
||||
std::uint32_t jobs{0};
|
||||
std::uint32_t candidates{0};
|
||||
std::uint32_t skippedbuckets{0};
|
||||
std::uint64_t skippedinstances{0};
|
||||
std::uint64_t sentrecords{0};
|
||||
bool restarted{false};
|
||||
};
|
||||
pass_counts m_counts;
|
||||
pass_counts m_lastcounts;
|
||||
bool m_dispatched{false};
|
||||
|
||||
std::vector<std::uint32_t> m_cpucounts; // of the current pass, by entry
|
||||
std::deque<cpu_pass> m_cpupasses;
|
||||
std::vector<std::uint32_t> m_readbackdata;
|
||||
comparison m_last;
|
||||
std::uint64_t m_comparedframes{0};
|
||||
std::uint64_t m_mismatchedframes{0};
|
||||
// draws of the last pass drawn from the gpu
|
||||
std::uint32_t m_draws{0};
|
||||
std::uint32_t m_prepassdraws{0};
|
||||
std::uint32_t m_calls{0};
|
||||
std::uint32_t m_prepasscalls{0};
|
||||
std::uint32_t m_commandcount{0};
|
||||
double m_cputime{0.0};
|
||||
bool m_occlusion{false}; // the last dispatch tested occlusion
|
||||
bool m_twophase{false}; // in two phases
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
648
rendering/common/localshadows.cpp
Normal file
648
rendering/common/localshadows.cpp
Normal file
@@ -0,0 +1,648 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/localshadows.h"
|
||||
|
||||
#include "rendering/openglcamera.h"
|
||||
#include "simulation/simulation.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "utilities/utilities.h"
|
||||
#include "vehicle/DynObj.h"
|
||||
#include "vehicle/Train.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// a lamp which moved farther than this (an animated submodel) has its scenery drawn anew, metres
|
||||
double const LAMP_MOVED{0.01};
|
||||
// lamps with a shorter reach aren't worth a map, metres
|
||||
float const LAMP_RANGE_MIN{1.f};
|
||||
|
||||
} // namespace
|
||||
|
||||
local_shadows::level_setup local_shadows::setup(int const Level)
|
||||
{
|
||||
switch (Level)
|
||||
{
|
||||
case 1:
|
||||
return {512, 4, 0.f, 1, 1024, 2, 8};
|
||||
case 2:
|
||||
return {1024, 2, 1.f, 1, 1024, 1, 4};
|
||||
case 3:
|
||||
return {1024, 1, 2.f, 2, 2048, 1, 2};
|
||||
default:
|
||||
return {0, 0, 0.f, 0, 0, 0, 0};
|
||||
}
|
||||
}
|
||||
|
||||
void local_shadows::allocate(int const Size)
|
||||
{
|
||||
release();
|
||||
if (Size <= 0)
|
||||
return;
|
||||
if (false == gpu_allocate(Size))
|
||||
{
|
||||
ErrorLog("lamp shadow map framebuffer setup failed, the lamps have no shadows");
|
||||
release();
|
||||
Global.gfx_lights_shadows = 0;
|
||||
return;
|
||||
}
|
||||
m_size = Size;
|
||||
WriteLog("lamp shadow maps: " + std::to_string(Size) + " x " + std::to_string(Size) + " cube");
|
||||
}
|
||||
|
||||
void local_shadows::release()
|
||||
{
|
||||
gpu_release();
|
||||
m_size = 0;
|
||||
m_valid = false;
|
||||
}
|
||||
|
||||
void local_shadows::allocate_lamps(int const Count, int const Size)
|
||||
{
|
||||
release_lamps();
|
||||
if ((Count <= 0) || (Size <= 0))
|
||||
return;
|
||||
// the scenery parts and the working copies
|
||||
if (false == gpu_allocate_lamps(Count, Size))
|
||||
{
|
||||
ErrorLog("scenery lamp shadow map framebuffer setup failed, the scenery lamps have no shadows");
|
||||
release_lamps();
|
||||
Global.gfx_lights_shadows_lamps = 0;
|
||||
return;
|
||||
}
|
||||
m_lampssize = Size;
|
||||
m_slots.assign(static_cast<std::size_t>(Count), lamp_slot{});
|
||||
WriteLog("scenery lamp shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " cube (" +
|
||||
std::to_string(2 * Count * 6 * Size / 1024 * Size * 4 / 1024) + " MB)");
|
||||
}
|
||||
|
||||
void local_shadows::release_lamps()
|
||||
{
|
||||
gpu_release_lamps();
|
||||
m_lampssize = 0;
|
||||
m_slots.clear();
|
||||
m_wanted.clear();
|
||||
}
|
||||
|
||||
void local_shadows::allocate_headlights(int const Count, int const Size)
|
||||
{
|
||||
release_headlights();
|
||||
if ((Count <= 0) || (Size <= 0))
|
||||
return;
|
||||
if (false == gpu_allocate_headlights(Count, Size))
|
||||
{
|
||||
ErrorLog("headlight shadow map framebuffer setup failed, the headlights have no shadows");
|
||||
release_headlights();
|
||||
Global.gfx_lights_shadows_headlights = 0;
|
||||
return;
|
||||
}
|
||||
m_headlightssize = Size;
|
||||
m_headlightslots.assign(static_cast<std::size_t>(Count), headlight_slot{});
|
||||
WriteLog("headlight shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " (" +
|
||||
std::to_string(Count * Size / 1024 * Size * 4 / 1024) + " MB)");
|
||||
}
|
||||
|
||||
void local_shadows::release_headlights()
|
||||
{
|
||||
gpu_release_headlights();
|
||||
m_headlightssize = 0;
|
||||
m_headlightslots.clear();
|
||||
m_headlightswanted.clear();
|
||||
}
|
||||
|
||||
bool local_shadows::update_wanted(TDynamicObject const *Vehicle, TModel3d const *Cab, glm::vec3 const &Lamp, unsigned int const Framestamp)
|
||||
{
|
||||
auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)};
|
||||
if (level != m_level)
|
||||
{
|
||||
m_level = level;
|
||||
auto const size{setup(level).size};
|
||||
if (size != m_size)
|
||||
allocate(size);
|
||||
m_valid = false;
|
||||
}
|
||||
if ((m_size == 0) || (Vehicle == nullptr) || (Cab == nullptr))
|
||||
{
|
||||
// with the lamp off nothing reads the map; once it is on again the cab may have changed
|
||||
m_valid = false;
|
||||
return false;
|
||||
}
|
||||
if ((false == m_valid) || (Vehicle != m_vehicle) || (Cab != m_cab) || (Lamp != m_lamp))
|
||||
{
|
||||
m_valid = false;
|
||||
}
|
||||
// the moving parts of the cab are followed every few frames
|
||||
if (m_valid && (Framestamp - m_drawnframe < setup(level).interval))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_vehicle = Vehicle;
|
||||
m_cab = Cab;
|
||||
m_lamp = Lamp;
|
||||
m_drawnframe = Framestamp;
|
||||
return true;
|
||||
}
|
||||
|
||||
void local_shadows::begin()
|
||||
{
|
||||
m_cpustart = std::chrono::steady_clock::now();
|
||||
gpu_begin_cab();
|
||||
}
|
||||
|
||||
void local_shadows::end(std::uint32_t const Drawcalls)
|
||||
{
|
||||
gpu_end_cab();
|
||||
m_valid = true;
|
||||
m_drawcalls = Drawcalls;
|
||||
++m_updatecount;
|
||||
auto const elapsed{std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - m_cpustart).count()};
|
||||
m_cputime = (m_cputime == 0.0 ? elapsed : m_cputime * 0.9 + elapsed * 0.1);
|
||||
}
|
||||
|
||||
glm::vec4 local_shadows::weights() const
|
||||
{
|
||||
// a face spans twice the distance from the lamp
|
||||
return glm::vec4{static_cast<float>(cab_slot), shadow_near, 2.f / static_cast<float>(std::max(m_size, 1)), setup(m_level).radius};
|
||||
}
|
||||
|
||||
void local_shadows::update_lamps(std::vector<gfx::local_lights::light> const &Lights, glm::dvec3 const &Camera, std::uint64_t const Version, unsigned int const Framestamp)
|
||||
{
|
||||
m_frame = Framestamp;
|
||||
m_wanted.clear();
|
||||
auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)};
|
||||
auto const count{level > 0 ? Global.gfx_lights_shadows_lamps : 0};
|
||||
auto const size{count > 0 ? Global.gfx_lights_shadows_lamps_size : 0};
|
||||
if ((static_cast<std::size_t>(count) != m_slots.size()) || (size != m_lampssize))
|
||||
{
|
||||
allocate_lamps(count, size);
|
||||
}
|
||||
if (m_slots.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_sceneryleft = setup(level).sceneryperframe;
|
||||
auto const reach{static_cast<double>(Global.gfx_lights_shadows_lamps_range)};
|
||||
// the lights come the most important first: the first ones within reach get the slots
|
||||
std::vector<gfx::local_lights::light const *> picked;
|
||||
for (auto const &light : Lights)
|
||||
{
|
||||
if (picked.size() >= m_slots.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
if ((light.owner == nullptr) || (light.range < LAMP_RANGE_MIN) || (glm::length(light.position - Camera) > reach))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
picked.emplace_back(&light);
|
||||
}
|
||||
// lamps which already have a slot keep it
|
||||
std::vector<gfx::local_lights::light const *> unplaced;
|
||||
for (auto const *light : picked)
|
||||
{
|
||||
auto placed{false};
|
||||
for (std::size_t idx = 0; idx < m_slots.size(); ++idx)
|
||||
{
|
||||
auto &slot{m_slots[idx]};
|
||||
if ((slot.owner == light->owner) && (slot.part == light->part) && (slot.wantedframe != Framestamp))
|
||||
{
|
||||
if (glm::length(slot.position - light->position) > LAMP_MOVED)
|
||||
{
|
||||
slot.scenery = false;
|
||||
slot.vehicles = false;
|
||||
}
|
||||
slot.position = light->position;
|
||||
slot.range = light->range;
|
||||
slot.wantedframe = Framestamp;
|
||||
m_wanted.emplace_back(idx);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (false == placed)
|
||||
{
|
||||
unplaced.emplace_back(light);
|
||||
}
|
||||
}
|
||||
// the others take the slots of lamps no longer picked
|
||||
for (auto const *light : unplaced)
|
||||
{
|
||||
for (std::size_t idx = 0; idx < m_slots.size(); ++idx)
|
||||
{
|
||||
auto &slot{m_slots[idx]};
|
||||
if (slot.wantedframe == Framestamp)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
slot = lamp_slot{};
|
||||
slot.owner = light->owner;
|
||||
slot.part = light->part;
|
||||
slot.position = light->position;
|
||||
slot.range = light->range;
|
||||
slot.wantedframe = Framestamp;
|
||||
m_wanted.emplace_back(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// the scene changed (events, the editor): the scenery parts are out of date
|
||||
for (auto &slot : m_slots)
|
||||
{
|
||||
if (slot.scenery && (slot.version != Version))
|
||||
{
|
||||
slot.scenery = false;
|
||||
slot.vehicles = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void local_shadows::reset_lamps()
|
||||
{
|
||||
for (auto &slot : m_slots)
|
||||
{
|
||||
slot = lamp_slot{};
|
||||
}
|
||||
m_wanted.clear();
|
||||
}
|
||||
|
||||
bool local_shadows::scenery_wanted(std::size_t const Slot)
|
||||
{
|
||||
if (m_slots[Slot].scenery || (m_sceneryleft <= 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
--m_sceneryleft;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool local_shadows::vehicles_wanted(std::size_t const Slot, unsigned int const Framestamp) const
|
||||
{
|
||||
auto const &slot{m_slots[Slot]};
|
||||
return slot.scenery && ((false == slot.vehicles) || (Framestamp - slot.vehiclesframe >= setup(m_level).interval));
|
||||
}
|
||||
|
||||
void local_shadows::begin_lamp(std::size_t const Slot, bool const Vehicles, std::uint64_t const Version, unsigned int const Framestamp)
|
||||
{
|
||||
auto &slot{m_slots[Slot]};
|
||||
auto const scenerycube{static_cast<int>(Slot)};
|
||||
auto const workingcube{static_cast<int>(m_slots.size() + Slot)};
|
||||
if (Vehicles)
|
||||
{
|
||||
// the vehicles go over the scenery
|
||||
slot.vehicles = true;
|
||||
slot.vehiclesframe = Framestamp;
|
||||
++m_lampsvehicles;
|
||||
}
|
||||
else
|
||||
{
|
||||
slot.scenery = true;
|
||||
slot.version = Version;
|
||||
slot.vehicles = false;
|
||||
++m_lampsscenery;
|
||||
}
|
||||
gpu_begin_lamp(scenerycube, workingcube, Vehicles);
|
||||
}
|
||||
|
||||
void local_shadows::end_lamp(std::uint32_t const Drawcalls)
|
||||
{
|
||||
gpu_end_lamp();
|
||||
m_lampsframedrawcalls += Drawcalls;
|
||||
}
|
||||
|
||||
void local_shadows::lamps_timer_begin()
|
||||
{
|
||||
m_lampsframedrawcalls = 0;
|
||||
m_cpustart = std::chrono::steady_clock::now();
|
||||
gpu_lamps_timer(true);
|
||||
m_lampstiming = true;
|
||||
}
|
||||
|
||||
void local_shadows::lamps_timer_end()
|
||||
{
|
||||
if (false == m_lampstiming)
|
||||
{
|
||||
return;
|
||||
}
|
||||
gpu_lamps_timer(false);
|
||||
m_lampstiming = false;
|
||||
m_lampsdrawcalls = m_lampsframedrawcalls;
|
||||
auto const elapsed{std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - m_cpustart).count()};
|
||||
m_lampscputime = (m_lampscputime == 0.0 ? elapsed : m_lampscputime * 0.9 + elapsed * 0.1);
|
||||
}
|
||||
|
||||
local_shadows::lamp_slot const *local_shadows::lamp(gfx::local_lights::light const &Light, std::size_t &Slot) const
|
||||
{
|
||||
if (Light.owner == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
for (auto const idx : m_wanted)
|
||||
{
|
||||
auto const &slot{m_slots[idx]};
|
||||
if ((slot.owner == Light.owner) && (slot.part == Light.part))
|
||||
{
|
||||
if (false == slot.scenery)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
Slot = idx;
|
||||
return &slot;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
glm::vec4 local_shadows::lamp_weights(std::size_t const Slot) const
|
||||
{
|
||||
auto const cube{m_slots[Slot].vehicles ? m_slots.size() + Slot : Slot};
|
||||
return glm::vec4{static_cast<float>(cube), shadow_near, 2.f / static_cast<float>(std::max(m_lampssize, 1)), setup(m_level).radius};
|
||||
}
|
||||
|
||||
std::vector<TDynamicObject *> const &local_shadows::lamp_vehicles(glm::dvec3 const &Camera)
|
||||
{
|
||||
m_lampvehicles.clear();
|
||||
auto const vehiclesrange{static_cast<double>(Global.gfx_lights_shadows_vehicles_range)};
|
||||
if (vehiclesrange > 0.0)
|
||||
{
|
||||
for (auto *vehicle : simulation::Vehicles.sequence())
|
||||
{
|
||||
if (glm::length(vehicle->GetPosition() - Camera) <= vehiclesrange + vehicle->radius())
|
||||
m_lampvehicles.emplace_back(vehicle);
|
||||
}
|
||||
}
|
||||
return m_lampvehicles;
|
||||
}
|
||||
|
||||
std::vector<TDynamicObject *> const &local_shadows::slot_vehicles(lamp_slot const &Slot)
|
||||
{
|
||||
m_slotvehicles.clear();
|
||||
for (auto *vehicle : m_lampvehicles)
|
||||
{
|
||||
if (glm::length(vehicle->GetPosition() - Slot.position) <= Slot.range + vehicle->radius())
|
||||
m_slotvehicles.emplace_back(vehicle);
|
||||
}
|
||||
return m_slotvehicles;
|
||||
}
|
||||
|
||||
std::vector<light_array::light_record const *> const &local_shadows::pick_headlights(light_array const &Lights, opengl_camera const &Camera)
|
||||
{
|
||||
m_headlightpicked.clear();
|
||||
auto const count{(Global.gfx_lights_headlights && (std::clamp(Global.gfx_lights_shadows, 0, 3) > 0)) ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast<int>(gl::MAX_HEADLIGHT_SHADOWS)) : 0};
|
||||
if (count > 0)
|
||||
{
|
||||
m_headlightcandidates.clear();
|
||||
auto const camera{Camera.position()};
|
||||
auto const range{static_cast<double>(Global.gfx_lights_shadows_headlights_range)};
|
||||
for (auto const &light : Lights.data)
|
||||
{
|
||||
if (light.intensity == 0.f)
|
||||
continue;
|
||||
// as in the light buffers: by day the light around takes the beam down to nothing
|
||||
auto luminance{static_cast<float>(Global.fLuminance)};
|
||||
if (light.owner->fShade > 0.f)
|
||||
luminance *= light.owner->fShade;
|
||||
auto const beam{light.color - glm::vec3{luminance}};
|
||||
if (std::max(beam.r, std::max(beam.g, beam.b)) <= 0.f)
|
||||
continue;
|
||||
// the part of the beam the shadows are cast in: from the end of the vehicle to the reach ahead of it
|
||||
auto const &dimensions{light.owner->MoverParameters->Dim};
|
||||
auto const radius{range * 0.5 + std::max(dimensions.W, dimensions.H) + 2.0};
|
||||
auto const centre{light.position + glm::dvec3{light.direction} * (range * 0.5 + 2.0)};
|
||||
if (glm::length(centre - camera) - radius > range)
|
||||
continue;
|
||||
if (false == Camera.visible(scene::bounding_area{centre, static_cast<float>(radius)}))
|
||||
continue;
|
||||
m_headlightcandidates.emplace_back(static_cast<float>(glm::length2(camera - light.position)) / light.intensity, &light);
|
||||
}
|
||||
auto const picked{std::min(m_headlightcandidates.size(), static_cast<std::size_t>(count))};
|
||||
std::partial_sort(std::begin(m_headlightcandidates), std::begin(m_headlightcandidates) + picked, std::end(m_headlightcandidates),
|
||||
[](auto const &Left, auto const &Right) { return Left.first < Right.first; });
|
||||
for (std::size_t idx = 0; idx < picked; ++idx)
|
||||
{
|
||||
m_headlightpicked.emplace_back(m_headlightcandidates[idx].second);
|
||||
}
|
||||
}
|
||||
return m_headlightpicked;
|
||||
}
|
||||
|
||||
void local_shadows::update_headlights(std::vector<light_array::light_record const *> const &Lights, unsigned int const Framestamp)
|
||||
{
|
||||
m_headlightswanted.clear();
|
||||
auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)};
|
||||
auto const count{level > 0 ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast<int>(gl::MAX_HEADLIGHT_SHADOWS)) : 0};
|
||||
auto const size{count > 0 ? setup(level).headlightsize : 0};
|
||||
if ((static_cast<std::size_t>(count) != m_headlightslots.size()) || (size != m_headlightssize))
|
||||
{
|
||||
allocate_headlights(count, size);
|
||||
}
|
||||
if (m_headlightslots.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// sets which already have a slot keep it
|
||||
std::vector<light_array::light_record const *> unplaced;
|
||||
for (auto const *light : Lights)
|
||||
{
|
||||
if (m_headlightswanted.size() + unplaced.size() >= m_headlightslots.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
auto placed{false};
|
||||
for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx)
|
||||
{
|
||||
auto &slot{m_headlightslots[idx]};
|
||||
if ((slot.owner == light->owner) && (slot.index == light->index) && (slot.wantedframe != Framestamp))
|
||||
{
|
||||
slot.wantedframe = Framestamp;
|
||||
m_headlightswanted.emplace_back(idx);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (false == placed)
|
||||
{
|
||||
unplaced.emplace_back(light);
|
||||
}
|
||||
}
|
||||
// the others take the slots of sets no longer picked
|
||||
for (auto const *light : unplaced)
|
||||
{
|
||||
for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx)
|
||||
{
|
||||
auto &slot{m_headlightslots[idx]};
|
||||
if (slot.wantedframe == Framestamp)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
slot = headlight_slot{};
|
||||
slot.owner = light->owner;
|
||||
slot.index = light->index;
|
||||
slot.wantedframe = Framestamp;
|
||||
m_headlightswanted.emplace_back(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void local_shadows::reset_headlights()
|
||||
{
|
||||
for (auto &slot : m_headlightslots)
|
||||
{
|
||||
slot = headlight_slot{};
|
||||
}
|
||||
m_headlightswanted.clear();
|
||||
}
|
||||
|
||||
bool local_shadows::headlight_wanted(std::size_t const Slot, bool const Moved, unsigned int const Framestamp) const
|
||||
{
|
||||
auto const &slot{m_headlightslots[Slot]};
|
||||
auto const level{setup(m_level)};
|
||||
return (false == slot.drawn) || (Framestamp - slot.drawnframe >= (Moved ? level.headlightmoving : level.headlightstill));
|
||||
}
|
||||
|
||||
void local_shadows::begin_headlight(std::size_t const Slot, glm::dvec3 const &Projector, glm::mat3 const &Rotation, glm::mat4 const &Projection, glm::vec3 const &Direction, unsigned int const Framestamp)
|
||||
{
|
||||
auto &slot{m_headlightslots[Slot]};
|
||||
slot.drawn = true;
|
||||
slot.drawnframe = Framestamp;
|
||||
slot.projector = Projector;
|
||||
slot.rotation = Rotation;
|
||||
slot.projection = Projection;
|
||||
slot.direction = Direction;
|
||||
++m_headlightsdrawn;
|
||||
gpu_begin_headlight(Slot);
|
||||
}
|
||||
|
||||
void local_shadows::end_headlight(std::uint32_t const Drawcalls)
|
||||
{
|
||||
gpu_end_headlight();
|
||||
m_headlightsframedrawcalls += Drawcalls;
|
||||
}
|
||||
|
||||
void local_shadows::headlights_timer_begin()
|
||||
{
|
||||
m_headlightsframedrawcalls = 0;
|
||||
m_headlightscpustart = std::chrono::steady_clock::now();
|
||||
gpu_headlights_timer(true);
|
||||
m_headlightstiming = true;
|
||||
}
|
||||
|
||||
void local_shadows::headlights_timer_end()
|
||||
{
|
||||
if (false == m_headlightstiming)
|
||||
{
|
||||
return;
|
||||
}
|
||||
gpu_headlights_timer(false);
|
||||
m_headlightstiming = false;
|
||||
m_headlightsdrawcalls = m_headlightsframedrawcalls;
|
||||
auto const elapsed{std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - m_headlightscpustart).count()};
|
||||
m_headlightscputime = (m_headlightscputime == 0.0 ? elapsed : m_headlightscputime * 0.9 + elapsed * 0.1);
|
||||
}
|
||||
|
||||
int local_shadows::headlight_map(TDynamicObject const *Owner, int const Index) const
|
||||
{
|
||||
for (auto const idx : m_headlightswanted)
|
||||
{
|
||||
auto const &slot{m_headlightslots[idx]};
|
||||
if ((slot.owner == Owner) && (slot.index == Index))
|
||||
{
|
||||
return (slot.drawn ? static_cast<int>(idx) : -1);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
glm::vec4 local_shadows::headlight_params(std::size_t const Slot) const
|
||||
{
|
||||
// a map spans 2 tan(fov / 2) one metre from the projector, and the projection holds 1 / tan(fov / 2)
|
||||
auto const &projection{m_headlightslots[Slot].projection};
|
||||
return glm::vec4{2.f / (std::max(projection[1][1], 1e-6f) * static_cast<float>(std::max(m_headlightssize, 1))), setup(m_level).radius, 0.f, 0.f};
|
||||
}
|
||||
|
||||
TDynamicObject *local_shadows::shadow_cab()
|
||||
{
|
||||
if (FreeFlyModeFlag || (false == Global.render_cab) || (simulation::Train == nullptr))
|
||||
return nullptr;
|
||||
auto *cab{simulation::Train->Dynamic()};
|
||||
if ((cab != nullptr) && ((false == cab->bDisplayCab) || (cab->mdKabina == nullptr) || (cab->mdKabina == cab->mdModel)))
|
||||
cab = nullptr;
|
||||
return cab;
|
||||
}
|
||||
|
||||
std::string local_shadows::info() const
|
||||
{
|
||||
std::string text;
|
||||
if (m_size != 0)
|
||||
{
|
||||
text += "\nlamp shadows: " + std::to_string(m_size) + " cube, " + (m_valid ? "cab lamp" : "none") + ", " + std::to_string(m_drawcalls) + " drawcalls, cpu " + to_string(m_cputime, 2) +
|
||||
" ms";
|
||||
}
|
||||
if (false == m_slots.empty())
|
||||
{
|
||||
auto withvehicles{0};
|
||||
auto ready{0};
|
||||
for (auto const idx : m_wanted)
|
||||
{
|
||||
ready += (m_slots[idx].scenery ? 1 : 0);
|
||||
withvehicles += (m_slots[idx].vehicles ? 1 : 0);
|
||||
}
|
||||
text += "\nscenery lamp shadows: " + std::to_string(m_wanted.size()) + " of " + std::to_string(m_slots.size()) + " slots (" + std::to_string(ready) + " ready, " +
|
||||
std::to_string(withvehicles) + " with vehicles), drawn " + std::to_string(m_lampsscenery) + " scenery + " + std::to_string(m_lampsvehicles) + " vehicles, " +
|
||||
std::to_string(m_lampsdrawcalls) + " drawcalls, cpu " + to_string(m_lampscputime, 2) + " ms";
|
||||
}
|
||||
if (false == m_headlightslots.empty())
|
||||
{
|
||||
auto ready{0};
|
||||
for (auto const idx : m_headlightswanted)
|
||||
{
|
||||
ready += (m_headlightslots[idx].drawn ? 1 : 0);
|
||||
}
|
||||
text += "\nheadlight shadows: " + std::to_string(m_headlightswanted.size()) + " of " + std::to_string(m_headlightslots.size()) + " slots (" + std::to_string(ready) + " ready), " +
|
||||
std::to_string(m_headlightssize) + " map, drawn " + std::to_string(m_headlightsdrawn) + " times, " + std::to_string(m_headlightsdrawcalls) + " drawcalls, cpu " +
|
||||
to_string(m_headlightscputime, 2) + " ms";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
void local_shadows::draw_debug_ui()
|
||||
{
|
||||
char const *levels[]{"no", "low (512, every 4th frame)", "medium (1024, every 2nd frame)", "high (1024, every frame, softer)"};
|
||||
ImGui::Combo("lamp shadows (gfx.lights.shadows)", &Global.gfx_lights_shadows, levels, IM_ARRAYSIZE(levels));
|
||||
if (m_size != 0)
|
||||
{
|
||||
ImGui::Text("cab: cube %d x %d, %s, drawn %u times, %u drawcalls, %.2f ms (cpu)", m_size, m_size, m_valid ? "cab lamp" : "no lamp", m_updatecount, m_drawcalls, m_cputime);
|
||||
}
|
||||
ImGui::SliderInt("scenery lamps with shadows (gfx.lights.shadows.lamps)", &Global.gfx_lights_shadows_lamps, 0, 32);
|
||||
ImGui::SliderFloat("their reach from the camera, m (.lamps.range)", &Global.gfx_lights_shadows_lamps_range, 0.f, 500.f);
|
||||
ImGui::SliderFloat("vehicles casting them, from the camera, m (.vehicles.range)", &Global.gfx_lights_shadows_vehicles_range, 0.f, 500.f);
|
||||
if (false == m_slots.empty())
|
||||
{
|
||||
auto const text{info()};
|
||||
auto const line{text.find("\nscenery lamp shadows:")};
|
||||
auto const end{(line == std::string::npos) ? std::string::npos : text.find('\n', line + 1)};
|
||||
if (line != std::string::npos)
|
||||
ImGui::TextUnformatted(text.substr(line + 1, end == std::string::npos ? std::string::npos : end - line - 1).c_str());
|
||||
}
|
||||
ImGui::SliderInt("headlights with shadows (gfx.lights.shadows.headlights)", &Global.gfx_lights_shadows_headlights, 0, static_cast<int>(gl::MAX_HEADLIGHT_SHADOWS));
|
||||
ImGui::SliderFloat("their reach ahead of the vehicle, m (.headlights.range)", &Global.gfx_lights_shadows_headlights_range, 10.f, 1000.f);
|
||||
if (false == m_headlightslots.empty())
|
||||
{
|
||||
auto const text{info()};
|
||||
auto const line{text.rfind('\n')};
|
||||
ImGui::TextUnformatted(text.c_str() + (line == std::string::npos ? 0 : line + 1));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
288
rendering/common/localshadows.h
Normal file
288
rendering/common/localshadows.h
Normal file
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
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 <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rendering/common/locallights.h"
|
||||
#include "rendering/lightarray.h"
|
||||
|
||||
class TDynamicObject;
|
||||
class TModel3d;
|
||||
class opengl_camera;
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
// the shadow maps of the lamps (Global.gfx_lights_shadows), the part shared by the renderers (stage P4 of
|
||||
// claude/vulkan14-renderer-plan.md): which lamps and headlights get maps, when they are drawn, what the shading reads; the
|
||||
// backend (opengl46_local_shadows, the vulkan renderer) keeps the maps and draws to them through the gpu_* methods.
|
||||
// - the lamp of the occupied cab, with the shadows cast by the cab (the model drawn by Render_cab()). the map is kept in the space
|
||||
// of the cab model, centred on the lamp: it stays right while the vehicle moves and turns, and is drawn anew only every few frames
|
||||
// (for the moving parts of the cab: levers, doors, wipers) or when the lamp, the cab or the level change;
|
||||
// - the lamps of scenery models closest to the camera (Global.gfx_lights_shadows_lamps of them, within
|
||||
// Global.gfx_lights_shadows_lamps_range). the map of such a lamp is kept in world axes, centred on the lamp, and has two parts:
|
||||
// the scenery (drawn once, when the lamp gets its slot or the scene changes) and a working copy of it, with the vehicles near
|
||||
// the camera (within Global.gfx_lights_shadows_vehicles_range) and the occupied cab added, made every few frames while there
|
||||
// are any by the lamp. the shading reads the working copy then, the scenery part otherwise.
|
||||
// the six faces of a map are drawn at once, so a model costs one draw call per submodel, as in the colour pass. the depth is
|
||||
// n / d (d the distance along the axis of the face), see local_light_shadow() in light_common.glsl, which reads the map for the
|
||||
// light with the layer in its headlight_weights.x, from the array of scenery lamps when its linear is 1 and from the one of the
|
||||
// cab lamp otherwise.
|
||||
// - the headlights of the vehicles (Global.gfx_lights_shadows_headlights sets of them, the most important ones): a plain map,
|
||||
// drawn from the projector of the beam with its projection (the beam is a narrow frustum aimed along the vehicle), with what is
|
||||
// within Global.gfx_lights_shadows_headlights_range ahead of the vehicle, but not the vehicle itself. the map keeps the projector
|
||||
// it was drawn with, in world space: one a few frames old is still right for what doesn't move. read by headlight_beam_shadow()
|
||||
// in light_common.glsl, for the headlights with the map in their headlight_weights.w (1 + the layer) and the projection to it in
|
||||
// gl::scene_ubs::headlight_shadow
|
||||
class local_shadows
|
||||
{
|
||||
public:
|
||||
// near plane of the maps, metres. surfaces closer to the lamp (its own fitting) cast no shadow
|
||||
static constexpr float shadow_near{0.15f};
|
||||
// layer of the map of the cab lamp in its array
|
||||
static constexpr int cab_slot{0};
|
||||
|
||||
local_shadows() = default;
|
||||
virtual ~local_shadows() = default;
|
||||
local_shadows(const local_shadows &) = delete;
|
||||
local_shadows &operator=(const local_shadows &) = delete;
|
||||
|
||||
// the cab lamp
|
||||
|
||||
// whether the map of the cab lamp is to be drawn in this frame: Vehicle and Cab the occupied vehicle and its cab model,
|
||||
// Lamp the position of the lamp in the space of the cab model; Vehicle nullptr when the lamp is off. (re)makes the map to
|
||||
// the size of the level when the level changed
|
||||
bool update_wanted(TDynamicObject const *Vehicle, TModel3d const *Cab, glm::vec3 const &Lamp, unsigned int const Framestamp);
|
||||
// sets up the drawing to the map: the target, viewport, cleared faces, the state of the faces. the geometry is to be
|
||||
// drawn with the programs of the cube maps and a modelview from the space of the model to the space of the map
|
||||
void begin();
|
||||
// ends the drawing to the map
|
||||
void end(std::uint32_t const Drawcalls);
|
||||
// whether the map holds the shadows of the lamp of Vehicle, the occupied vehicle
|
||||
bool ready(TDynamicObject const *Vehicle) const
|
||||
{
|
||||
return m_valid && (Vehicle != nullptr) && (Vehicle == m_vehicle);
|
||||
}
|
||||
// position of the lamp the map was drawn for, in the space of the cab model
|
||||
glm::vec3 const &lamp() const
|
||||
{
|
||||
return m_lamp;
|
||||
}
|
||||
// the headlight_weights of the light which reads the map: layer, near plane, size of a texel one metre from the lamp,
|
||||
// radius of the filter in texels
|
||||
glm::vec4 weights() const;
|
||||
|
||||
// lamps of scenery models
|
||||
|
||||
// a lamp with a slot
|
||||
struct lamp_slot
|
||||
{
|
||||
void const *owner{nullptr}; // the light it is, see gfx::local_lights::light
|
||||
void const *part{nullptr};
|
||||
glm::dvec3 position{0.0}; // world space
|
||||
float range{0.f}; // of the light, metres
|
||||
unsigned int wantedframe{~0u}; // framestamp it was last among the lamps given shadows
|
||||
bool scenery{false}; // the scenery part of the map is drawn
|
||||
std::uint64_t version{0}; // of the scene the scenery part was drawn for
|
||||
bool vehicles{false}; // the working copy is drawn, with vehicles, and is what the shading reads
|
||||
unsigned int vehiclesframe{0}; // framestamp the working copy was last drawn in
|
||||
};
|
||||
// picks the lamps which get shadows in this frame from the gathered lights (the most important first): the scenery lamps
|
||||
// within reach of Camera. a lamp keeps its slot while it is picked; one which got a slot or moved, and all of them when the
|
||||
// scene changes (Version), need their scenery drawn. (re)makes the maps when their count or size changed
|
||||
void update_lamps(std::vector<gfx::local_lights::light> const &Lights, glm::dvec3 const &Camera, std::uint64_t const Version, unsigned int const Framestamp);
|
||||
// forgets the lamps: the scene they come from is about to be replaced (a scenery being loaded)
|
||||
void reset_lamps();
|
||||
// slots of the lamps picked in this frame
|
||||
std::vector<std::size_t> const &lamps() const
|
||||
{
|
||||
return m_wanted;
|
||||
}
|
||||
lamp_slot const &slot(std::size_t const Slot) const
|
||||
{
|
||||
return m_slots[Slot];
|
||||
}
|
||||
// whether the scenery part of the map of the slot is to be drawn in this frame. a few per frame at most, by the level
|
||||
bool scenery_wanted(std::size_t const Slot);
|
||||
// whether the working copy of the slot, with vehicles, is to be drawn in this frame (every few frames, by the level)
|
||||
bool vehicles_wanted(std::size_t const Slot, unsigned int const Framestamp) const;
|
||||
// the slot has no vehicles by it: the shading reads the scenery part
|
||||
void no_vehicles(std::size_t const Slot)
|
||||
{
|
||||
m_slots[Slot].vehicles = false;
|
||||
}
|
||||
// sets up the drawing to the scenery part (cleared) or to the working copy (a copy of the scenery part) of the map of the slot.
|
||||
// the geometry is to be drawn with the programs of the cube maps and a modelview to world axes centred on the lamp
|
||||
void begin_lamp(std::size_t const Slot, bool const Vehicles, std::uint64_t const Version, unsigned int const Framestamp);
|
||||
// ends the drawing to the map of a lamp
|
||||
void end_lamp(std::uint32_t const Drawcalls);
|
||||
// the timing of the drawing of the maps of the lamps in this frame
|
||||
void lamps_timer_begin();
|
||||
void lamps_timer_end();
|
||||
// the slot of the lamp the light is, if it has its shadows ready, nullptr otherwise
|
||||
lamp_slot const *lamp(gfx::local_lights::light const &Light, std::size_t &Slot) const;
|
||||
// the headlight_weights of the light of the lamp in the slot, see weights()
|
||||
glm::vec4 lamp_weights(std::size_t const Slot) const;
|
||||
// the vehicles near the camera, which cast the shadows of the lamps (Global.gfx_lights_shadows_vehicles_range), and those by
|
||||
// the lamp of a slot, within its reach; valid until the next call
|
||||
std::vector<TDynamicObject *> const &lamp_vehicles(glm::dvec3 const &Camera);
|
||||
std::vector<TDynamicObject *> const &slot_vehicles(lamp_slot const &Slot);
|
||||
std::vector<TDynamicObject *> const &slot_vehicles() const
|
||||
{
|
||||
return m_slotvehicles;
|
||||
}
|
||||
|
||||
// headlights of vehicles
|
||||
|
||||
// a set of headlights (one end of a vehicle) with a slot
|
||||
struct headlight_slot
|
||||
{
|
||||
TDynamicObject const *owner{nullptr}; // light_array::light_record::owner and index
|
||||
int index{-1};
|
||||
unsigned int wantedframe{~0u}; // framestamp it was last among the headlights given shadows
|
||||
bool drawn{false}; // the map holds the shadows of the headlights
|
||||
unsigned int drawnframe{0};
|
||||
// the projector the map was drawn with: world space position, world axes to its axes, projection
|
||||
glm::dvec3 projector{0.0};
|
||||
glm::mat3 rotation{1.f};
|
||||
glm::mat4 projection{1.f};
|
||||
glm::vec3 direction{0.f}; // of the light when the map was drawn
|
||||
};
|
||||
// the headlights which shine, the most important first (in the order the light buffers take them), whose beam may be seen
|
||||
// from the camera of the colour pass within the reach of the shadows, as many as there are to be slots; valid until the next call
|
||||
std::vector<light_array::light_record const *> const &pick_headlights(light_array const &Lights, opengl_camera const &Camera);
|
||||
// picks the headlights which get shadows in this frame: Lights, the most important first, as many as there are slots
|
||||
// (Global.gfx_lights_shadows_headlights). a set keeps its slot while it is picked. (re)makes the maps when their count or size changed
|
||||
void update_headlights(std::vector<light_array::light_record const *> const &Lights, unsigned int const Framestamp);
|
||||
// forgets the headlights: the vehicles may be about to go (a scenery being loaded)
|
||||
void reset_headlights();
|
||||
// slots of the headlights picked in this frame
|
||||
std::vector<std::size_t> const &headlights() const
|
||||
{
|
||||
return m_headlightswanted;
|
||||
}
|
||||
headlight_slot const &headlight(std::size_t const Slot) const
|
||||
{
|
||||
return m_headlightslots[Slot];
|
||||
}
|
||||
// whether the map of the slot is to be drawn in this frame: one never drawn, then every few frames by the level, more often
|
||||
// while the headlights Moved (with the vehicle)
|
||||
bool headlight_wanted(std::size_t const Slot, bool const Moved, unsigned int const Framestamp) const;
|
||||
// sets up the drawing to the map of the slot (cleared), drawn from the projector given (see headlight_slot). the geometry is
|
||||
// to be drawn by the shadow pass with this projection, back faces only
|
||||
void begin_headlight(std::size_t const Slot, glm::dvec3 const &Projector, glm::mat3 const &Rotation, glm::mat4 const &Projection, glm::vec3 const &Direction, unsigned int const Framestamp);
|
||||
// ends the drawing to the map of a set of headlights
|
||||
void end_headlight(std::uint32_t const Drawcalls);
|
||||
// the timing of the drawing of the maps of the headlights in this frame
|
||||
void headlights_timer_begin();
|
||||
void headlights_timer_end();
|
||||
// the slot of the headlights with their map ready in this frame, -1 if there is none
|
||||
int headlight_map(TDynamicObject const *Owner, int const Index) const;
|
||||
// the headlight_shadow_params of the map of the slot, see gl::scene_ubs
|
||||
glm::vec4 headlight_params(std::size_t const Slot) const;
|
||||
|
||||
// the occupied vehicle whose cab casts the shadows of the lamps and headlights, nullptr when it's not drawn
|
||||
static TDynamicObject *shadow_cab();
|
||||
|
||||
// lines for the statistics of the debug panel
|
||||
std::string info() const;
|
||||
// controls for the debug window
|
||||
void draw_debug_ui();
|
||||
|
||||
protected:
|
||||
// size, refresh interval (frames), filter radius (texels, 0: the 2x2 filter of the hardware) of a level, the number of
|
||||
// scenery parts of lamp maps drawn per frame, and the size and refresh intervals (frames, while the vehicle moves and while
|
||||
// it doesn't) of the maps of headlights
|
||||
struct level_setup
|
||||
{
|
||||
int size;
|
||||
unsigned int interval;
|
||||
float radius;
|
||||
int sceneryperframe;
|
||||
int headlightsize;
|
||||
unsigned int headlightmoving;
|
||||
unsigned int headlightstill;
|
||||
};
|
||||
static level_setup setup(int const Level);
|
||||
|
||||
// what the backend does with the maps: makes them (false: they can't be made) and lets them go; the cab lamp: one cube, the
|
||||
// lamps: 2 x Count cubes (the scenery parts, then the working copies), the headlights: Count layers
|
||||
virtual bool gpu_allocate(int const Size) = 0;
|
||||
virtual void gpu_release() = 0;
|
||||
virtual bool gpu_allocate_lamps(int const Count, int const Size) = 0;
|
||||
virtual void gpu_release_lamps() = 0;
|
||||
virtual bool gpu_allocate_headlights(int const Count, int const Size) = 0;
|
||||
virtual void gpu_release_headlights() = 0;
|
||||
// the drawing to the map of the cab lamp, to a cube of the lamps (Vehicles: the working copy, from a copy of the scenery part
|
||||
// Scenerycube; the scenery part cleared otherwise) and to a layer of the headlights
|
||||
virtual void gpu_begin_cab() = 0;
|
||||
virtual void gpu_end_cab() = 0;
|
||||
virtual void gpu_begin_lamp(int const Scenerycube, int const Workingcube, bool const Vehicles) = 0;
|
||||
virtual void gpu_end_lamp() = 0;
|
||||
virtual void gpu_begin_headlight(std::size_t const Slot) = 0;
|
||||
virtual void gpu_end_headlight() = 0;
|
||||
// the gpu times of the drawings of a frame
|
||||
virtual void gpu_lamps_timer(bool const Begin) = 0;
|
||||
virtual void gpu_headlights_timer(bool const Begin) = 0;
|
||||
|
||||
void allocate(int const Size);
|
||||
void release();
|
||||
void allocate_lamps(int const Count, int const Size);
|
||||
void release_lamps();
|
||||
void allocate_headlights(int const Count, int const Size);
|
||||
void release_headlights();
|
||||
|
||||
int m_level{-1}; // level the maps were made for
|
||||
// cab lamp
|
||||
int m_size{0}; // of a face of the map, 0 when there is none
|
||||
bool m_valid{false}; // the map holds the shadows of the lamp of m_vehicle
|
||||
TDynamicObject const *m_vehicle{nullptr};
|
||||
TModel3d const *m_cab{nullptr};
|
||||
glm::vec3 m_lamp{0.f};
|
||||
unsigned int m_drawnframe{0}; // framestamp the map was last drawn in
|
||||
// scenery lamps: the scenery parts of the slots first, then their working copies
|
||||
int m_lampssize{0};
|
||||
std::vector<lamp_slot> m_slots;
|
||||
std::vector<std::size_t> m_wanted;
|
||||
unsigned int m_frame{0}; // framestamp of the last update_lamps()
|
||||
int m_sceneryleft{0}; // scenery parts which may still be drawn in this frame
|
||||
bool m_lampstiming{false};
|
||||
std::vector<TDynamicObject *> m_lampvehicles;
|
||||
std::vector<TDynamicObject *> m_slotvehicles;
|
||||
// headlights: a layer per slot
|
||||
int m_headlightssize{0};
|
||||
std::vector<headlight_slot> m_headlightslots;
|
||||
std::vector<std::size_t> m_headlightswanted;
|
||||
bool m_headlightstiming{false};
|
||||
std::chrono::steady_clock::time_point m_headlightscpustart;
|
||||
std::vector<std::pair<float, light_array::light_record const *>> m_headlightcandidates;
|
||||
std::vector<light_array::light_record const *> m_headlightpicked;
|
||||
// statistics
|
||||
std::uint32_t m_drawcalls{0}; // of the last drawing of the cab map
|
||||
std::uint32_t m_updatecount{0}; // drawings of the cab map so far
|
||||
double m_cputime{0.0}; // milliseconds, smoothed
|
||||
std::chrono::steady_clock::time_point m_cpustart;
|
||||
std::uint32_t m_lampsscenery{0}; // scenery parts drawn so far
|
||||
std::uint32_t m_lampsvehicles{0}; // working copies drawn so far
|
||||
std::uint32_t m_lampsdrawcalls{0}; // in the drawings of the lamp maps in the last frame with any
|
||||
std::uint32_t m_lampsframedrawcalls{0};
|
||||
double m_lampscputime{0.0}; // milliseconds per frame, smoothed
|
||||
std::uint32_t m_headlightsdrawn{0}; // maps of headlights drawn so far
|
||||
std::uint32_t m_headlightsdrawcalls{0}; // in the drawings of the maps of headlights in the last frame with any
|
||||
std::uint32_t m_headlightsframedrawcalls{0};
|
||||
double m_headlightscputime{0.0}; // milliseconds per frame, smoothed
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
@@ -26,7 +26,10 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "world/TrackSleepers.h"
|
||||
#include "model/material.h"
|
||||
#include "gl/ubo.h"
|
||||
#include "rendering/common/instancecull.h"
|
||||
|
||||
#include <array>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -230,32 +233,65 @@ class scene_walk
|
||||
virtual void walk_region_begin() {}
|
||||
// the opaque ground of the colour pass is drawn, the editor takes the position of the cursor from the depth
|
||||
virtual void walk_editor_cursor() {}
|
||||
// a bucket of instances of a cell: false when the backend took the instances (the walk leaves them out); Area is the cell's
|
||||
virtual bool walk_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, bool const Bucketrange, scene::bounding_area const &Area)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// a bucket of instances of a cell: false when the gpu culling took the instances (the walk leaves them out); Area is the cell's
|
||||
bool walk_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, bool const Bucketrange, scene::bounding_area const &Area);
|
||||
// a bucket left out of the pass as wholly out of sight
|
||||
virtual void walk_bucket_culled(scene::basic_cell::instance_bucket const &Bucket) {}
|
||||
// a model drawn on its own: true when the backend took it. Inrange: the cell of the instance lies within the range of the pass
|
||||
virtual bool walk_queue_model(TAnimModel *Instance, bool const Inrange)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// the sleepers of a track: true when the backend took them
|
||||
virtual bool walk_queue_sleepers(TTrack *Track)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void walk_bucket_culled(scene::basic_cell::instance_bucket const &Bucket);
|
||||
// a model drawn on its own: true when the gpu culling took it. Inrange: the cell of the instance lies within the range of the pass
|
||||
bool walk_queue_model(TAnimModel *Instance, bool const Inrange);
|
||||
// the sleepers of a track: true when the gpu culling took them
|
||||
bool walk_queue_sleepers(TTrack *Track);
|
||||
// the sleepers the walk drew for a track, by their squared distances, and the lod bounds of their model
|
||||
virtual void walk_sleepers_drawn(TTrack const &Track, std::vector<float> const &Lodbounds, std::vector<std::pair<float, glm::mat4>> const &Drawn) {}
|
||||
void walk_sleepers_drawn(TTrack const &Track, std::vector<float> const &Lodbounds, std::vector<std::pair<float, glm::mat4>> const &Drawn);
|
||||
// the instances of the cells are about to be drawn: whether the walk reports what it drew of each bucket (walk_instances_compare())
|
||||
virtual bool walk_instances_begin()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
virtual void walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector<std::uint32_t> const &Bandcounts) {}
|
||||
void walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector<std::uint32_t> const &Bandcounts);
|
||||
virtual void walk_instances_end() {}
|
||||
// the geometry of the backend and whether it draws blended now, for the draws recorded for the gpu culling
|
||||
virtual gfx::geometrybank_manager &walk_geometry() = 0;
|
||||
virtual bool walk_blending_enabled() const = 0;
|
||||
|
||||
// the gpu culling of the instances (Global.gfx_gpucull, stage N6c): what the walk queues in the culling of the pass and the
|
||||
// draws it records for the indirect draws made from its result. the backend picks the culling of the pass, runs it and
|
||||
// makes the draws
|
||||
// starts the culling of the pass, or none (nullptr). Mainview: the culling of the main view, which takes the sleepers
|
||||
void Begin_Pass_Cull(gfx::instance_cull *Cull, bool const Mainview);
|
||||
// with the cpu drawing (Global.gfx_gpucull 2): what it drew of the sleepers and of the models drawn on their own, for the
|
||||
// comparison with the culling dispatched already
|
||||
void Compare_Pass_Cull();
|
||||
// ends the culling of the pass
|
||||
void End_Pass_Cull();
|
||||
// queues the sleepers of a track in the gpu culling of the pass (Global.gfx_gpucull_sleepers), with the gates of
|
||||
// Render_Sleepers(), making their records first when they're missing or out of date
|
||||
void Queue_Sleepers(TTrack *Track);
|
||||
// queues a model instance drawn on its own in the gpu culling of the pass (Global.gfx_gpucull_models), as a bucket of
|
||||
// its own, making its record first when it's missing or out of date. Inrange: the cell of the instance lies within the
|
||||
// range of the pass. returns true when the instance isn't drawn by the cpu in the pass: it's drawn from the result of the
|
||||
// culling, or it was culled and recorded by the depth pass of the whole scene, or it lies out of range
|
||||
bool Queue_Model(TAnimModel *Instance, bool const Inrange);
|
||||
// whether a model with its skins has any part the sun shadows draw: the same test Render_Instanced() makes. the answer
|
||||
// is kept for the rest of the pass
|
||||
bool Instances_Cast_Shadow(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket);
|
||||
// the walk of the submodel trees of the groups culled by Cull, once per lod band, which records their draws
|
||||
// (m_indirectdraws, m_indirectcommands). Cascade: the pass is one of the cascades of the sun shadows, which share the walks
|
||||
void Walk_Indirect_Draws(gfx::instance_cull const &Cull, bool const Cascade);
|
||||
// a draw of the walk of Walk_Indirect_Draws(), with the state the walk left for it (model_ubs with the modelview set)
|
||||
void Record_Indirect_Draw(gfx::geometry_handle const &Geometry);
|
||||
// multi draws of the recorded draws (Global.gfx_gpucull_multidraw): puts the draws in an order in which the ones with the
|
||||
// same state follow one another, the draws of one entry kept in the order of the walk
|
||||
void Order_Indirect_Draws();
|
||||
// whether two recorded draws can go in one multi draw of the colour pass: the same material and texture variant, geometry
|
||||
// bank, primitive and blending
|
||||
bool Indirect_Mergeable(std::size_t const First, std::size_t const Other) const;
|
||||
// the texture the shadow material binds for the alpha test of a material, or null_handle (no test)
|
||||
texture_handle Indirect_Shadow_Texture(material_handle const Material) const;
|
||||
// the records of the sleepers and of the models drawn on their own not queued for a while are let go
|
||||
void Expire_Indirect_Records();
|
||||
// a scenery is being loaded: the tracks and the models the records were made for may be gone
|
||||
void Clear_Indirect_Records();
|
||||
|
||||
// members
|
||||
renderpass_config m_renderpass; // parameters for current render pass
|
||||
@@ -320,6 +356,150 @@ class scene_walk
|
||||
scene::basic_cell::instance_bucket_key const *m_modelcomparekey{nullptr};
|
||||
std::vector<std::pair<scene::basic_cell::instance_bucket_key, float>> m_modelcpudistances;
|
||||
bool m_indirectchanging{false}; // the walk under way met a submodel which may look different in the next frame
|
||||
// the gpu culling of the pass under way (Begin_Pass_Cull()), and whether there's one
|
||||
gfx::instance_cull *m_passcull{nullptr};
|
||||
bool m_instancecullpass{false};
|
||||
bool m_modelspass{false}; // the models drawn on their own of the pass under way go to its gpu culling
|
||||
bool m_indirectcolourmodels{false}; // with m_indirectcolourpending: the depth pass culled and recorded them too
|
||||
bool m_sleeperspass{false}; // the sleepers of the pass under way go to its gpu culling (the one of the main view)
|
||||
bool m_indirectcoloursleepers{false}; // with m_indirectcolourpending: the depth pass culled and recorded the sleepers too
|
||||
// the instances were culled in the depth pass of the whole scene, the colour pass draws them from the recorded draws
|
||||
bool m_indirectcolourpending{false};
|
||||
// Instances_Cast_Shadow() of the groups of the shadow pass under way
|
||||
std::unordered_map<scene::basic_cell::instance_bucket_key, bool, scene::basic_cell::instance_bucket_key_hash> m_shadowcasters;
|
||||
// the sleepers of tracks culled on the gpu with the instances of the main view (Global.gfx_gpucull_sleepers): records made
|
||||
// from TTrack::m_sleeper_local_transforms when the track is queued first, and again when they or the instance records
|
||||
// (editor, m_instancerecordsversion) went out of date. the records of tracks not queued for a while are let go
|
||||
struct sleeper_records
|
||||
{
|
||||
scene::basic_cell::instance_bucket bucket; // the records only, no instances
|
||||
scene::basic_cell::instance_bucket_key key; // the model, and the skin in the slot Render_Sleepers() sets it in
|
||||
scene::bounding_area area; // of the locations of the sleepers
|
||||
TModel3d const *model{nullptr}; // what the records were made from
|
||||
material_handle skin{null_handle};
|
||||
glm::mat4 const *transforms{nullptr};
|
||||
std::size_t count{0};
|
||||
unsigned int frame{0}; // queued last
|
||||
};
|
||||
std::unordered_map<TTrack const *, sleeper_records> m_sleeperrecords;
|
||||
// the model instances drawn on their own culled on the gpu (Global.gfx_gpucull_models), each one a bucket of its own with
|
||||
// a group of its own: the record made when the instance is queued first, and again when the instance records went out of
|
||||
// date (editor, events: m_instancerecordsversion). the records of instances not queued for a while are let go
|
||||
struct model_records
|
||||
{
|
||||
scene::basic_cell::instance_bucket bucket; // the instance alone
|
||||
scene::basic_cell::instance_bucket_key key; // the model, the skins and the instance
|
||||
scene::bounding_area area; // the location of the instance
|
||||
unsigned int frame{0}; // queued last
|
||||
};
|
||||
std::unordered_map<TAnimModel const *, model_records> m_modelrecords;
|
||||
// counts per lod band of the sleepers Render_Sleepers() drew in the pass, by model and skin, for the comparison (they're
|
||||
// drawn before the culling runs)
|
||||
std::unordered_map<scene::basic_cell::instance_bucket_key, std::vector<std::uint32_t>, scene::basic_cell::instance_bucket_key_hash> m_sleeperbandcounts;
|
||||
// a draw recorded from the walk of the submodel tree of a group in one lod band (Walk_Indirect_Draws()), to be replayed once
|
||||
// the instances of its command are known: the state the walk left for it and what it bound
|
||||
struct indirect_draw
|
||||
{
|
||||
gl::model_ubs ubs;
|
||||
material_handle material;
|
||||
TSubModel const *submodel;
|
||||
gfx::geometry_handle geometry;
|
||||
unsigned int type; // primitive
|
||||
bool blending; // drawn with the transparency hack of submodels faded out
|
||||
std::uint32_t group{0}; // the buffers of the geometry, which the draws of a multi draw share (geometrybank_manager::draw_group())
|
||||
};
|
||||
std::vector<indirect_draw> m_indirectdraws;
|
||||
std::vector<gl::instance_draw_command> m_indirectcommands; // in the order of m_indirectdraws
|
||||
bool m_indirectrecording{false}; // the draws and the material binds of the backend record instead of drawing
|
||||
std::uint32_t m_indirectentry{0}; // entry (group and lod band) recorded
|
||||
material_handle m_indirectmaterial{null_handle}; // what the last material bind noted while recording
|
||||
TSubModel const *m_indirectsubmodel{nullptr};
|
||||
// the walks Walk_Indirect_Draws() made for the cascades of the sun shadows in the frame, for the other cascades to copy: the
|
||||
// draws and commands recorded for a group (model and skins) in each of its lod bands, first none where no cascade walked it
|
||||
struct cascade_band
|
||||
{
|
||||
std::uint32_t first;
|
||||
std::uint32_t count;
|
||||
};
|
||||
struct cascade_record
|
||||
{
|
||||
unsigned int frame{0};
|
||||
std::vector<cascade_band> bands;
|
||||
};
|
||||
std::unordered_map<scene::basic_cell::instance_bucket_key, cascade_record, scene::basic_cell::instance_bucket_key_hash> m_cascaderecords;
|
||||
std::vector<indirect_draw> m_cascadedraws;
|
||||
std::vector<gl::instance_draw_command> m_cascadecommands;
|
||||
unsigned int m_cascaderecordframe{0};
|
||||
// the walks Walk_Indirect_Draws() made in earlier frames (Global.gfx_gpucull_walkcache). what the walk of a group of instances
|
||||
// in a lod band records depends on the model with its skins, the band and whether the pass draws shadows, not on the camera,
|
||||
// so it's kept while the shared state of the model stays the same (TModel3d::state_version()) and used instead of walking the
|
||||
// tree again. a walk meeting an animated submodel, one lit by the luminance or the stars isn't kept. the models drawn on their
|
||||
// own (with an instance in the key) are walked every time
|
||||
struct walk_band
|
||||
{
|
||||
std::vector<indirect_draw> draws; // as recorded
|
||||
float bandmin{0.f};
|
||||
std::uint32_t modelversion{0};
|
||||
bool blending{false}; // the state the walk began in
|
||||
bool valid{false};
|
||||
};
|
||||
struct walk_record
|
||||
{
|
||||
unsigned int frame{0}; // used last
|
||||
std::array<std::vector<walk_band>, 2> bands; // of the colour passes (the main view, the environment map) and of the shadows
|
||||
};
|
||||
std::unordered_map<scene::basic_cell::instance_bucket_key, walk_record, scene::basic_cell::instance_bucket_key_hash> m_walkrecords;
|
||||
int m_walkrankcutoff{0}; // Global.gfx_shadow_rank_cutoff the walks were made with
|
||||
// what the walks of the frame did, for the panel: walks made, of them not kept, walks taken from earlier frames, and the cpu time
|
||||
// of the walks and of putting the draws in order with their blocks uploaded
|
||||
struct walk_counts
|
||||
{
|
||||
std::uint32_t walked{0};
|
||||
std::uint32_t changing{0};
|
||||
std::uint32_t reused{0};
|
||||
double walktime{0.0};
|
||||
double ordertime{0.0};
|
||||
};
|
||||
walk_counts m_walkcounts;
|
||||
walk_counts m_lastwalkcounts;
|
||||
unsigned int m_walkcountsframe{0};
|
||||
// appends the draws of a walk kept from an earlier frame to the recorded draws, with the commands of the entry under way
|
||||
void Replay_Walk(walk_band const &Walk, bool const Shadows);
|
||||
// working space of Order_Indirect_Draws(), kept between frames
|
||||
struct indirect_queue
|
||||
{
|
||||
std::uint32_t next; // draw
|
||||
std::uint32_t end;
|
||||
std::uint32_t link; // next queue waiting for the same state
|
||||
};
|
||||
struct indirect_state
|
||||
{
|
||||
std::uint32_t head;
|
||||
std::uint32_t tail;
|
||||
};
|
||||
// what a multi draw has in common, and the draw which has it
|
||||
struct indirect_state_key
|
||||
{
|
||||
material_handle material;
|
||||
std::uint32_t variant; // of the textures of the material
|
||||
std::uint32_t bank;
|
||||
unsigned int type;
|
||||
std::uint32_t indexed;
|
||||
bool blending;
|
||||
std::uint32_t draw;
|
||||
auto tie() const
|
||||
{
|
||||
return std::tie(material, variant, bank, type, indexed, blending);
|
||||
}
|
||||
};
|
||||
std::vector<indirect_state_key> m_indirectstatekeys;
|
||||
std::vector<std::uint32_t> m_indirectdrawstates;
|
||||
std::vector<indirect_queue> m_indirectqueues;
|
||||
std::vector<indirect_state> m_indirectstates;
|
||||
std::vector<std::uint32_t> m_indirectstateorder;
|
||||
std::vector<std::uint32_t> m_indirectorder;
|
||||
std::vector<indirect_draw> m_indirectdrawsordered;
|
||||
std::vector<gl::instance_draw_command> m_indirectcommandsordered;
|
||||
// a vehicle drawn in the colour pass of this frame moved (motion blur)
|
||||
bool m_vehiclesmoving{false};
|
||||
// set while a preview is drawn: submodels faded out entirely are skipped, instead of being drawn invisible over the stencil
|
||||
|
||||
829
rendering/common/scenewalkindirect.cpp
Normal file
829
rendering/common/scenewalkindirect.cpp
Normal file
@@ -0,0 +1,829 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
// the part of the shared walk of the scene which feeds the gpu culling of the instances and records the draws made from its
|
||||
// result (stage N6c of claude/vulkan14-renderer-plan.md): the records of the sleepers and of the models drawn on their own, the
|
||||
// queueing of buckets, the walks of the submodel trees of the culled groups and the order of the recorded draws
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/scenewalk.h"
|
||||
|
||||
#include "model/AnimModel.h"
|
||||
#include "model/Model3d.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/utilities.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// the group of the sleepers of a track in the gpu culling: their model, and the skin in the slot Render_Sleepers() sets it in
|
||||
scene::basic_cell::instance_bucket_key sleeper_key(TTrack const &Track)
|
||||
{
|
||||
scene::basic_cell::instance_bucket_key key;
|
||||
key.pModel = Track.m_sleeper_model;
|
||||
key.skins[1] = Track.m_sleeper_skin;
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void scene_walk::Begin_Pass_Cull(gfx::instance_cull *Cull, bool const Mainview)
|
||||
{
|
||||
m_passcull = Cull;
|
||||
m_instancecullpass = false;
|
||||
if (m_passcull != nullptr)
|
||||
{
|
||||
m_instancecullpass = true;
|
||||
// the same choices as in Render_Instanced()
|
||||
auto const reflections{m_renderpass.draw_mode == rendermode::reflections};
|
||||
gfx::instance_cull::pass_setup setup;
|
||||
setup.frame = m_framestamp;
|
||||
setup.camera = m_renderpass.pass_camera.position();
|
||||
setup.distancecamera = (m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera.position() : m_renderpass.pass_camera.position());
|
||||
setup.frustum = &m_renderpass.pass_camera.frustum();
|
||||
setup.distancescale = (reflections ? 1.f : static_cast<float>(1.0 / (static_cast<double>(Global.ZoomFactor) * static_cast<double>(Global.ZoomFactor) * static_cast<double>(Global.fDistanceFactor))));
|
||||
setup.distanceoffset = (reflections ? static_cast<float>(sq(EU07_REFLECTIONFIDELITYOFFSET)) : 0.f);
|
||||
setup.distancerange = (reflections ? static_cast<float>(sq(Global.reflectiontune.range_instances)) : std::numeric_limits<float>::max());
|
||||
setup.drawrange = static_cast<float>(sq(m_renderpass.draw_range + 250));
|
||||
setup.zoom = Global.ZoomFactor;
|
||||
setup.drawing = (Global.gfx_gpucull == 1);
|
||||
m_passcull->begin_pass(setup);
|
||||
}
|
||||
// the sleepers of tracks go with the instances of the main view
|
||||
m_sleeperspass = (Mainview && (m_passcull != nullptr) && Global.gfx_gpucull_sleepers);
|
||||
if (m_sleeperspass && (false == m_passcull->drawing()))
|
||||
{
|
||||
for (auto &counts : m_sleeperbandcounts)
|
||||
{
|
||||
counts.second.clear();
|
||||
}
|
||||
}
|
||||
// the models drawn on their own go with the instances of every pass culled on the gpu
|
||||
m_modelspass = (m_instancecullpass && Global.gfx_gpucull_models);
|
||||
}
|
||||
|
||||
void scene_walk::Compare_Pass_Cull()
|
||||
{
|
||||
// the sleepers the cpu drew along the tracks, before the culling ran
|
||||
if (m_sleeperspass)
|
||||
{
|
||||
for (auto const &counts : m_sleeperbandcounts)
|
||||
{
|
||||
if (false == counts.second.empty())
|
||||
{
|
||||
m_passcull->cpu_result(counts.first, counts.second, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// and the models drawn on their own
|
||||
for (auto const &model : m_modelcpudistances)
|
||||
{
|
||||
m_passcull->cpu_instance(model.first, model.second);
|
||||
}
|
||||
}
|
||||
|
||||
void scene_walk::End_Pass_Cull()
|
||||
{
|
||||
if (m_instancecullpass)
|
||||
{
|
||||
m_passcull->end_pass();
|
||||
m_instancecullpass = false;
|
||||
m_passcull = nullptr;
|
||||
}
|
||||
m_sleeperspass = false;
|
||||
m_modelspass = false;
|
||||
}
|
||||
|
||||
bool scene_walk::walk_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, bool const Bucketrange, scene::bounding_area const &Area)
|
||||
{
|
||||
if (m_instancecullpass)
|
||||
{
|
||||
// the gpu tests every instance of the bucket, the way Render_Instanced() does. models with nothing to draw in the
|
||||
// sun shadows are left out of them, as Render_Instanced() leaves them
|
||||
if ((m_renderpass.draw_mode != rendermode::shadows) || Instances_Cast_Shadow(Key, Bucket))
|
||||
{
|
||||
m_passcull->add_bucket(Key, Bucket, (Bucketrange ? Bucket_Area(Bucket) : Area));
|
||||
}
|
||||
// and when the instances are drawn from its result, the cpu has nothing more to do with them
|
||||
if (m_passcull->drawing())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// the depth pass of the whole scene culled them already, the colour pass draws what it recorded
|
||||
if (m_indirectcolourpending)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void scene_walk::walk_bucket_culled(scene::basic_cell::instance_bucket const &Bucket)
|
||||
{
|
||||
if (m_instancecullpass)
|
||||
{
|
||||
m_passcull->skip_bucket(Bucket);
|
||||
}
|
||||
}
|
||||
|
||||
bool scene_walk::walk_queue_model(TAnimModel *Instance, bool const Inrange)
|
||||
{
|
||||
return Queue_Model(Instance, Inrange);
|
||||
}
|
||||
|
||||
bool scene_walk::walk_queue_sleepers(TTrack *Track)
|
||||
{
|
||||
if (m_sleeperspass)
|
||||
{
|
||||
// culled with the instances of the pass and drawn from the result, or drawn by the cpu as well to compare
|
||||
Queue_Sleepers(Track);
|
||||
if (m_passcull->drawing())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (m_indirectcolourpending && m_indirectcoloursleepers)
|
||||
{
|
||||
// the depth pass of the whole scene culled and recorded them, the colour pass draws what it recorded
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void scene_walk::walk_sleepers_drawn(TTrack const &Track, std::vector<float> const &Lodbounds, std::vector<std::pair<float, glm::mat4>> const &Drawn)
|
||||
{
|
||||
if (m_sleeperspass && m_instancecullpass && (false == m_passcull->drawing()))
|
||||
{
|
||||
// the comparison with the gpu culling: the sleepers drawn by lod band, the bands as the culling makes them, split by
|
||||
// the bounds of the model past 0 and short of the largest float
|
||||
auto const counted = [](float const Bound) {
|
||||
return (Bound > 0.f) && (Bound < std::numeric_limits<float>::max());
|
||||
};
|
||||
auto &counts{m_sleeperbandcounts[sleeper_key(Track)]};
|
||||
auto const bands{static_cast<std::size_t>(std::count_if(Lodbounds.begin(), Lodbounds.end(), counted)) + 1};
|
||||
if (counts.size() < bands)
|
||||
{
|
||||
counts.resize(bands, 0);
|
||||
}
|
||||
for (auto const &survivor : Drawn)
|
||||
{
|
||||
std::size_t band{0};
|
||||
for (auto const bound : Lodbounds)
|
||||
{
|
||||
if (counted(bound) && (bound <= survivor.first))
|
||||
{
|
||||
++band;
|
||||
}
|
||||
}
|
||||
++counts[band];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void scene_walk::walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector<std::uint32_t> const &Bandcounts)
|
||||
{
|
||||
m_passcull->cpu_result(Key, Bandcounts);
|
||||
}
|
||||
|
||||
void scene_walk::Queue_Sleepers(TTrack *Track)
|
||||
{
|
||||
// the gates of Render_Sleepers(); the range is tested for the whole track
|
||||
if ((Track->m_sleeper_model == nullptr) || Track->m_sleeper_local_transforms.empty() || (Global.SleeperDistance <= 0.f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto const distsq{glm::length2(Track->location() - m_renderpass.pass_camera.position())};
|
||||
auto const cutoffsq{static_cast<double>(Global.SleeperDistance) * static_cast<double>(Global.SleeperDistance)};
|
||||
if (distsq > cutoffsq)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto &records{m_sleeperrecords[Track]};
|
||||
auto const &transforms{Track->m_sleeper_local_transforms};
|
||||
if ((records.bucket.recordsversion != m_instancerecordsversion) || (records.model != Track->m_sleeper_model) || (records.skin != Track->m_sleeper_skin) ||
|
||||
(records.transforms != transforms.data()) || (records.count != transforms.size()))
|
||||
{
|
||||
// each sleeper is a record placed and turned by its transform, relative to the origin of the track (the centre of its
|
||||
// cell), with the sphere Render_Sleepers() tests against the frustum
|
||||
auto const radius{std::max(Track->m_sleeper_model->bounding_radius(), 2.0f)};
|
||||
auto &bucket{records.bucket};
|
||||
bucket.records.clear();
|
||||
bucket.records.reserve(transforms.size());
|
||||
glm::vec3 low{std::numeric_limits<float>::max()};
|
||||
glm::vec3 high{std::numeric_limits<float>::lowest()};
|
||||
for (auto const &local : transforms)
|
||||
{
|
||||
scene::basic_cell::instance_record record;
|
||||
glm::vec3 const position{local[3]};
|
||||
record.position = packed_vec3{position};
|
||||
record.radius = radius;
|
||||
record.rangesquaredmin = 0.f;
|
||||
record.rangesquaredmax = std::numeric_limits<float>::max();
|
||||
record.flags = scene::basic_cell::instance_visible | scene::basic_cell::instance_instanceable;
|
||||
for (int column = 0; column < 3; ++column)
|
||||
{
|
||||
record.rotationscale[column] = local[column];
|
||||
}
|
||||
bucket.records.emplace_back(record);
|
||||
low = glm::min(low, position);
|
||||
high = glm::max(high, position);
|
||||
}
|
||||
bucket.recordorigin = Track->m_origin;
|
||||
bucket.lodrangesquared = std::numeric_limits<float>::max();
|
||||
bucket.recordsversion = m_instancerecordsversion;
|
||||
bucket.gpustale = true;
|
||||
records.key = sleeper_key(*Track);
|
||||
records.area = scene::bounding_area{Track->m_origin + glm::dvec3{(low + high) * 0.5f}, 0.5f * glm::length(high - low)};
|
||||
records.model = Track->m_sleeper_model;
|
||||
records.skin = Track->m_sleeper_skin;
|
||||
records.transforms = transforms.data();
|
||||
records.count = transforms.size();
|
||||
}
|
||||
records.frame = m_framestamp;
|
||||
m_passcull->add_sleepers(records.key, records.bucket, records.area);
|
||||
}
|
||||
|
||||
// queues a model instance drawn on its own in the gpu culling of the pass, as a bucket and a group of its own. the culling
|
||||
// makes the tests of Render( TAnimModel * ): visibility, frustum, the range of the instance, the draw range, the size on the
|
||||
// screen, and gives it the lod band of the bounds of its submodels (mindistance/maxdistance) its distance falls in, then
|
||||
// Walk_Indirect_Draws() animates, prepares and walks its submodel tree from its origin, like it does for a group of
|
||||
// instanced ones
|
||||
bool scene_walk::Queue_Model(TAnimModel *Instance, bool const Inrange)
|
||||
{
|
||||
// a billboard is turned to the camera from the modelview of the instance, which a walk from its origin doesn't have
|
||||
if ((false == Instance->m_cameraindependent) || (Instance->Model() == nullptr))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (false == m_modelspass)
|
||||
{
|
||||
// the depth pass of the whole scene culled and recorded it, the colour pass draws what it recorded
|
||||
return (m_indirectcolourpending && m_indirectcolourmodels);
|
||||
}
|
||||
auto const drawing{m_passcull->drawing()};
|
||||
// a model hidden by an event, or in a cell wholly out of the range of the pass, would be left out by the culling
|
||||
if ((false == Instance->visible()) || (false == Inrange))
|
||||
{
|
||||
return drawing;
|
||||
}
|
||||
|
||||
auto &records{m_modelrecords[Instance]};
|
||||
auto &bucket{records.bucket};
|
||||
if ((bucket.recordsversion != m_instancerecordsversion) || (bucket.records.size() != 1))
|
||||
{
|
||||
// the key, the instance and the record anew: the editor may have given the instance another model or skins, or
|
||||
// another instance may have taken the place of a deleted one
|
||||
auto &key{records.key};
|
||||
key = {};
|
||||
key.pModel = Instance->Model();
|
||||
if (auto const *material{Instance->Material()})
|
||||
{
|
||||
for (int skin = 0; skin < 5; ++skin)
|
||||
{
|
||||
key.skins[skin] = material->replacable_skins[skin];
|
||||
}
|
||||
}
|
||||
key.instance = Instance;
|
||||
bucket.instances.assign(1, Instance);
|
||||
// the location of the instance as the origin of the record, which keeps the precision
|
||||
Update_Instance_Records(key.pModel, bucket, Instance->location());
|
||||
// drawn by its bucket, though it isn't instanceable
|
||||
bucket.records.front().flags |= scene::basic_cell::instance_instanceable;
|
||||
records.area = scene::bounding_area{Instance->location(), 0.f};
|
||||
}
|
||||
records.frame = m_framestamp;
|
||||
if (m_renderpass.draw_mode == rendermode::shadows)
|
||||
{
|
||||
// a model with nothing to draw in the shadows is left out of them, like the instances. the answer depends on the
|
||||
// model and the skins only, and is shared by the instances with them
|
||||
auto modelkey{records.key};
|
||||
modelkey.instance = nullptr;
|
||||
if (false == Instances_Cast_Shadow(modelkey, bucket))
|
||||
{
|
||||
return drawing;
|
||||
}
|
||||
}
|
||||
// out of the frustum of the pass, beyond its range or too small on the screen by its record: the culling would leave it out,
|
||||
// and the walk of Walk_Indirect_Draws() isn't made for it
|
||||
if (Global.gfx_gpucull_bucketrange && (false == EditorModeFlag) && Bucket_Culled(bucket))
|
||||
{
|
||||
m_passcull->skip_bucket(bucket);
|
||||
return drawing;
|
||||
}
|
||||
m_passcull->add_bucket(records.key, bucket, records.area);
|
||||
if (false == drawing)
|
||||
{
|
||||
// the cpu draws it too, and tells what it drew for the comparison
|
||||
m_modelcomparekey = &records.key;
|
||||
}
|
||||
return drawing;
|
||||
}
|
||||
|
||||
bool scene_walk::Instances_Cast_Shadow(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket)
|
||||
{
|
||||
auto const lookup{m_shadowcasters.find(Key)};
|
||||
if (lookup != m_shadowcasters.end())
|
||||
{
|
||||
return lookup->second;
|
||||
}
|
||||
|
||||
// the skins of the instance Render_Instanced() would take them from
|
||||
TAnimModel const *instance{nullptr};
|
||||
for (auto const *candidate : Bucket.instances)
|
||||
{
|
||||
if (candidate != nullptr)
|
||||
{
|
||||
instance = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto const *skins{instance != nullptr && instance->Material() != nullptr ? instance->Material()->replacable_skins : nullptr};
|
||||
auto castsshadow{false};
|
||||
m_instance_lodpending.clear();
|
||||
m_instance_lodpending.push_back(Key.pModel != nullptr ? Key.pModel->Root : nullptr);
|
||||
while ((false == castsshadow) && (false == m_instance_lodpending.empty()))
|
||||
{
|
||||
auto const *submodel = m_instance_lodpending.back();
|
||||
m_instance_lodpending.pop_back();
|
||||
if (submodel == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (submodel->eType < TP_ROTATOR)
|
||||
{
|
||||
auto const material{submodel->m_material >= 0 ? submodel->m_material : (skins != nullptr ? skins[-submodel->m_material] : null_handle)};
|
||||
castsshadow = (walk_material_data(material).shadow_rank <= Global.gfx_shadow_rank_cutoff);
|
||||
}
|
||||
m_instance_lodpending.push_back(submodel->Child);
|
||||
m_instance_lodpending.push_back(submodel->Next);
|
||||
}
|
||||
m_shadowcasters.emplace(Key, castsshadow);
|
||||
return castsshadow;
|
||||
}
|
||||
|
||||
void scene_walk::Walk_Indirect_Draws(gfx::instance_cull const &Cull, bool const Cascade)
|
||||
{
|
||||
m_indirectdraws.clear();
|
||||
m_indirectcommands.clear();
|
||||
auto const &lodbounds{Cull.lod_bounds()};
|
||||
// the cascades of the sun shadows make the same walks: the mode, the lod band and the state of the models are the same in all
|
||||
// of them within a frame, only the instances differ, and those the gpu fills in. the walk of a group in a band is made by the
|
||||
// first cascade which draws it, the others copy its draws and commands with entries of their own
|
||||
auto const cascade{(m_renderpass.draw_mode == rendermode::shadows) && Cascade};
|
||||
auto constexpr nowalk{std::numeric_limits<std::uint32_t>::max()};
|
||||
if (cascade && (m_cascaderecordframe != m_framestamp))
|
||||
{
|
||||
m_cascaderecordframe = m_framestamp;
|
||||
m_cascadedraws.clear();
|
||||
m_cascadecommands.clear();
|
||||
if ((m_framestamp % 256) == 0)
|
||||
{
|
||||
// the groups not drawn for a while
|
||||
for (auto records{m_cascaderecords.begin()}; records != m_cascaderecords.end();)
|
||||
{
|
||||
if (m_framestamp - records->second.frame > 256)
|
||||
{
|
||||
records = m_cascaderecords.erase(records);
|
||||
}
|
||||
else
|
||||
{
|
||||
++records;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_walkcountsframe != m_framestamp)
|
||||
{
|
||||
m_walkcountsframe = m_framestamp;
|
||||
m_lastwalkcounts = m_walkcounts;
|
||||
m_walkcounts = {};
|
||||
}
|
||||
auto const walkstart{std::chrono::steady_clock::now()};
|
||||
// the walks of earlier frames. they go when the switch is off, as the models may change meanwhile without the walks noticing
|
||||
// it the next time (the season), and when the shadows take other submodels
|
||||
auto const walkcache{Global.gfx_gpucull_walkcache && (false == m_previewing)};
|
||||
if ((false == Global.gfx_gpucull_walkcache) || (m_walkrankcutoff != Global.gfx_shadow_rank_cutoff))
|
||||
{
|
||||
m_walkrecords.clear();
|
||||
m_walkrankcutoff = Global.gfx_shadow_rank_cutoff;
|
||||
}
|
||||
if (walkcache && ((m_framestamp % 256) == 0))
|
||||
{
|
||||
// the groups not drawn for a while
|
||||
for (auto records{m_walkrecords.begin()}; records != m_walkrecords.end();)
|
||||
{
|
||||
if (m_framestamp - records->second.frame > 256)
|
||||
{
|
||||
records = m_walkrecords.erase(records);
|
||||
}
|
||||
else
|
||||
{
|
||||
++records;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto const shadows{m_renderpass.draw_mode == rendermode::shadows};
|
||||
m_indirectrecording = true;
|
||||
for (auto const &group : Cull.draw_groups())
|
||||
{
|
||||
auto *model{group.key->pModel};
|
||||
if ((model == nullptr) || (model->Root == nullptr) || ((group.instance == nullptr) && (false == group.sleepers)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// a model drawn on its own is prepared with its lights and animations each time; the others are prepared only for a walk.
|
||||
// the version is taken before, so a walk prepared by something which changes the model isn't taken up again
|
||||
walk_record *kept{nullptr};
|
||||
if (walkcache && (group.key->instance == nullptr))
|
||||
{
|
||||
kept = &m_walkrecords[*group.key];
|
||||
kept->frame = m_framestamp;
|
||||
auto &bands{kept->bands[shadows ? 1 : 0]};
|
||||
if (bands.size() != group.lodcount + 1)
|
||||
{
|
||||
bands.clear();
|
||||
bands.resize(group.lodcount + 1);
|
||||
}
|
||||
}
|
||||
auto const modelversion{model->state_version()};
|
||||
material_handle const *skins{nullptr};
|
||||
auto alpha{0x30300030};
|
||||
auto prepared{false};
|
||||
auto const prepare = [&]() {
|
||||
prepared = true;
|
||||
if (group.sleepers)
|
||||
{
|
||||
// as Render_Sleepers() sets them up: the skin of the track, if any. like scenery models they pick no texture
|
||||
// variants of their own
|
||||
if (group.key->skins[1] != null_handle)
|
||||
{
|
||||
skins = group.key->skins.data();
|
||||
}
|
||||
TSubModel::iVariantSeed = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (group.key->instance != nullptr)
|
||||
{
|
||||
// a model drawn on its own: its lights and animations, the way Render( TAnimModel * ) sets them up
|
||||
group.instance->RaAnimate(m_framestamp);
|
||||
}
|
||||
group.instance->RaPrepare();
|
||||
auto const *material{group.instance->Material()};
|
||||
if (material != nullptr)
|
||||
{
|
||||
skins = material->replacable_skins;
|
||||
alpha = material->textures_alpha;
|
||||
}
|
||||
}
|
||||
alpha ^= 0x0F0F000F;
|
||||
};
|
||||
// the walks of the group the cascades made in this frame. a group whose bands were all walked already isn't prepared
|
||||
cascade_record *walks{nullptr};
|
||||
if (cascade)
|
||||
{
|
||||
walks = &m_cascaderecords[*group.key];
|
||||
if ((walks->frame != m_framestamp) || (walks->bands.size() != group.lodcount + 1))
|
||||
{
|
||||
walks->frame = m_framestamp;
|
||||
walks->bands.assign(group.lodcount + 1, cascade_band{nowalk, 0});
|
||||
}
|
||||
}
|
||||
else if (kept == nullptr)
|
||||
{
|
||||
prepare();
|
||||
}
|
||||
for (std::uint32_t band = 0; band <= group.lodcount; ++band)
|
||||
{
|
||||
// a band holds the squared distances from its lower bound up to the next one; any of them selects the same lod
|
||||
auto const bandmin{band == 0 ? 0.f : lodbounds[group.lodfirst + band - 1]};
|
||||
auto const bandmax{band == group.lodcount ? std::numeric_limits<float>::max() : lodbounds[group.lodfirst + band]};
|
||||
if ((bandmin > group.distancemax) || (bandmax <= group.distancemin))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
m_indirectentry = group.entryfirst + band;
|
||||
if ((walks != nullptr) && (walks->bands[band].first != nowalk))
|
||||
{
|
||||
// walked by another cascade: its draws, for the instances of this entry
|
||||
auto const &walk{walks->bands[band]};
|
||||
m_indirectdraws.insert(m_indirectdraws.end(), m_cascadedraws.begin() + walk.first, m_cascadedraws.begin() + walk.first + walk.count);
|
||||
for (std::uint32_t draw = 0; draw < walk.count; ++draw)
|
||||
{
|
||||
auto &command{m_indirectcommands.emplace_back(m_cascadecommands[walk.first + draw])};
|
||||
command.entry = m_indirectentry;
|
||||
}
|
||||
// as the walk would count them
|
||||
m_renderpass.draw_stats.submodels += static_cast<int>(walk.count);
|
||||
m_renderpass.draw_stats.drawcalls += static_cast<int>(walk.count);
|
||||
continue;
|
||||
}
|
||||
auto const walkfirst{m_indirectdraws.size()};
|
||||
auto *keptwalk{kept != nullptr ? &kept->bands[shadows ? 1 : 0][band] : nullptr};
|
||||
if ((keptwalk != nullptr) && (keptwalk->valid) && (keptwalk->bandmin == bandmin) && (keptwalk->modelversion == modelversion) &&
|
||||
(keptwalk->blending == walk_blending_enabled()))
|
||||
{
|
||||
// walked in an earlier frame, with the model as it is now
|
||||
Replay_Walk(*keptwalk, shadows);
|
||||
++m_walkcounts.reused;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (false == prepared)
|
||||
{
|
||||
prepare();
|
||||
}
|
||||
auto const blending{walk_blending_enabled()};
|
||||
m_indirectchanging = false;
|
||||
::glPushMatrix();
|
||||
::glLoadIdentity();
|
||||
model->Root->fSquareDist = bandmin;
|
||||
model->Root->ReplacableSet(skins, alpha);
|
||||
model->Root->pRoot = model;
|
||||
Render(model->Root);
|
||||
::glPopMatrix();
|
||||
++m_walkcounts.walked;
|
||||
if (m_indirectchanging)
|
||||
{
|
||||
++m_walkcounts.changing;
|
||||
}
|
||||
if (keptwalk != nullptr)
|
||||
{
|
||||
keptwalk->valid = (false == m_indirectchanging);
|
||||
if (keptwalk->valid)
|
||||
{
|
||||
keptwalk->draws.assign(m_indirectdraws.begin() + walkfirst, m_indirectdraws.end());
|
||||
keptwalk->bandmin = bandmin;
|
||||
keptwalk->modelversion = modelversion;
|
||||
keptwalk->blending = blending;
|
||||
}
|
||||
else
|
||||
{
|
||||
keptwalk->draws.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (walks != nullptr)
|
||||
{
|
||||
walks->bands[band] = cascade_band{static_cast<std::uint32_t>(m_cascadedraws.size()), static_cast<std::uint32_t>(m_indirectdraws.size() - walkfirst)};
|
||||
m_cascadedraws.insert(m_cascadedraws.end(), m_indirectdraws.begin() + walkfirst, m_indirectdraws.end());
|
||||
m_cascadecommands.insert(m_cascadecommands.end(), m_indirectcommands.begin() + walkfirst, m_indirectcommands.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
m_indirectrecording = false;
|
||||
m_walkcounts.walktime += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - walkstart).count();
|
||||
}
|
||||
|
||||
void scene_walk::Record_Indirect_Draw(gfx::geometry_handle const &Geometry)
|
||||
{
|
||||
// the walk of Walk_Indirect_Draws(): the draw is made later, from the gpu culling, with this state
|
||||
auto &geometry{walk_geometry()};
|
||||
gfx::indirect_source source;
|
||||
if (geometry.indirect_source(Geometry, source))
|
||||
{
|
||||
m_indirectcommands.push_back({source.count, 0, source.first, static_cast<std::uint32_t>(source.basevertex), 0, m_indirectentry, (source.indexed ? 1u : 0u), 0});
|
||||
m_indirectdraws.push_back({model_ubs, m_indirectmaterial, m_indirectsubmodel, Geometry, source.type, walk_blending_enabled(), geometry.draw_group(Geometry)});
|
||||
}
|
||||
}
|
||||
|
||||
void scene_walk::Replay_Walk(walk_band const &Walk, bool const Shadows)
|
||||
{
|
||||
auto &geometry{walk_geometry()};
|
||||
for (auto const &walkdraw : Walk.draws)
|
||||
{
|
||||
// the place of the chunk in its bank is asked for again: the bank may have been let go since and made anew, which the
|
||||
// question (as the walk) takes care of, uploading the chunk
|
||||
gfx::indirect_source source;
|
||||
if (false == geometry.indirect_source(walkdraw.geometry, source))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
m_indirectcommands.push_back({source.count, 0, source.first, static_cast<std::uint32_t>(source.basevertex), 0, m_indirectentry, (source.indexed ? 1u : 0u), 0});
|
||||
auto &draw{m_indirectdraws.emplace_back(walkdraw)};
|
||||
draw.type = source.type;
|
||||
draw.group = geometry.draw_group(walkdraw.geometry);
|
||||
// the walk sets the placement of the submodel, and before each draw of the colour passes the parameters of its material
|
||||
// (the material bind); the rest of the block is what the pass has at the moment, as the walk would leave it
|
||||
auto const &walked{walkdraw.ubs};
|
||||
draw.ubs = model_ubs;
|
||||
draw.ubs.modelview = walked.modelview;
|
||||
draw.ubs.modelviewnormal = walked.modelviewnormal;
|
||||
if (false == Shadows)
|
||||
{
|
||||
std::memcpy(&draw.ubs.param[0], &walked.param[0], sizeof(draw.ubs.param));
|
||||
draw.ubs.opacity = walked.opacity;
|
||||
draw.ubs.alpha_mult = walked.alpha_mult;
|
||||
}
|
||||
}
|
||||
// as the walk would count them
|
||||
m_renderpass.draw_stats.submodels += static_cast<int>(Walk.draws.size());
|
||||
m_renderpass.draw_stats.drawcalls += static_cast<int>(Walk.draws.size());
|
||||
}
|
||||
|
||||
texture_handle scene_walk::Indirect_Shadow_Texture(material_handle const Material) const
|
||||
{
|
||||
if (Material == null_handle)
|
||||
{
|
||||
return null_handle;
|
||||
}
|
||||
return walk_material_data(Material).textures[0];
|
||||
}
|
||||
|
||||
// the order of the recorded draws for multi draws. draws go in one multi draw when they follow one another with the same
|
||||
// state (material and its texture variant, geometry bank, primitive, blending; Indirect_Mergeable()). the draws of one entry,
|
||||
// the walk of one submodel tree in one lod band, keep their order: submodels of a model may lie on one another and rely on
|
||||
// the later one being drawn over the earlier one. the draws of different entries are drawn for different instances, which
|
||||
// the gpu draws in no particular order anyway, so they're free to move. the entries are queues of draws; the draws at the
|
||||
// heads of the queues which have the state taken next all go, each queue as far as its draws keep that state, then the
|
||||
// queues wait for the state of their next draw. the states go in the order in which the queues first reach them
|
||||
void scene_walk::Order_Indirect_Draws()
|
||||
{
|
||||
auto const count{m_indirectdraws.size()};
|
||||
if (count < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// a number for each state the draws have, by sorting the states (the buffers are kept between frames)
|
||||
m_indirectstatekeys.clear();
|
||||
// the sun shadows draw with the program and the texture of the alpha test, whatever the material
|
||||
auto const shadows{m_renderpass.draw_mode == rendermode::shadows};
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
auto const &draw{m_indirectdraws[index]};
|
||||
if (shadows)
|
||||
{
|
||||
m_indirectstatekeys.push_back({Indirect_Shadow_Texture(draw.material), 0u, draw.group, draw.type, m_indirectcommands[index].indexed, draw.blending, static_cast<std::uint32_t>(index)});
|
||||
continue;
|
||||
}
|
||||
auto variant{0u};
|
||||
if (draw.material != null_handle)
|
||||
{
|
||||
auto const &material{walk_material_data(draw.material)};
|
||||
if (false == material.texture_variants.empty())
|
||||
{
|
||||
variant = TSubModel::VariantId(draw.submodel) % static_cast<std::uint32_t>(material.texture_variants.size());
|
||||
}
|
||||
}
|
||||
m_indirectstatekeys.push_back({draw.material, variant, draw.group, draw.type, m_indirectcommands[index].indexed, draw.blending, static_cast<std::uint32_t>(index)});
|
||||
}
|
||||
std::sort(std::begin(m_indirectstatekeys), std::end(m_indirectstatekeys), [](indirect_state_key const &Left, indirect_state_key const &Right) { return Left.tie() < Right.tie(); });
|
||||
m_indirectdrawstates.resize(count);
|
||||
std::uint32_t statecount{0};
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
if ((index > 0) && (m_indirectstatekeys[index].tie() != m_indirectstatekeys[index - 1].tie()))
|
||||
{
|
||||
++statecount;
|
||||
}
|
||||
m_indirectdrawstates[m_indirectstatekeys[index].draw] = statecount;
|
||||
}
|
||||
++statecount;
|
||||
if (statecount == count)
|
||||
{
|
||||
// no two draws share a state, nothing would go together
|
||||
return;
|
||||
}
|
||||
|
||||
auto constexpr none{std::numeric_limits<std::uint32_t>::max()};
|
||||
m_indirectstates.assign(statecount, indirect_state{none, none});
|
||||
m_indirectstateorder.clear();
|
||||
m_indirectqueues.clear();
|
||||
// the queue waits for the state of its next draw, behind the queues waiting for it already
|
||||
auto const enqueue = [this, none](std::uint32_t const Queue) {
|
||||
auto &queue{m_indirectqueues[Queue]};
|
||||
auto &state{m_indirectstates[m_indirectdrawstates[queue.next]]};
|
||||
queue.link = none;
|
||||
if (state.head == none)
|
||||
{
|
||||
state.head = Queue;
|
||||
m_indirectstateorder.push_back(m_indirectdrawstates[queue.next]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_indirectqueues[state.tail].link = Queue;
|
||||
}
|
||||
state.tail = Queue;
|
||||
};
|
||||
for (std::size_t index = 0; index < count;)
|
||||
{
|
||||
auto end{index + 1};
|
||||
while ((end < count) && (m_indirectcommands[end].entry == m_indirectcommands[index].entry))
|
||||
{
|
||||
++end;
|
||||
}
|
||||
m_indirectqueues.push_back({static_cast<std::uint32_t>(index), static_cast<std::uint32_t>(end), none});
|
||||
index = end;
|
||||
}
|
||||
for (std::uint32_t queue = 0; queue < m_indirectqueues.size(); ++queue)
|
||||
{
|
||||
enqueue(queue);
|
||||
}
|
||||
m_indirectorder.clear();
|
||||
for (std::size_t taken = 0; taken < m_indirectstateorder.size(); ++taken)
|
||||
{
|
||||
auto const stateindex{m_indirectstateorder[taken]};
|
||||
auto queueindex{m_indirectstates[stateindex].head};
|
||||
m_indirectstates[stateindex] = {none, none};
|
||||
while (queueindex != none)
|
||||
{
|
||||
auto &queue{m_indirectqueues[queueindex]};
|
||||
auto const nextqueue{queue.link};
|
||||
while ((queue.next < queue.end) && (m_indirectdrawstates[queue.next] == stateindex))
|
||||
{
|
||||
m_indirectorder.push_back(queue.next);
|
||||
++queue.next;
|
||||
}
|
||||
if (queue.next < queue.end)
|
||||
{
|
||||
enqueue(queueindex);
|
||||
}
|
||||
queueindex = nextqueue;
|
||||
}
|
||||
}
|
||||
|
||||
m_indirectdrawsordered.clear();
|
||||
m_indirectcommandsordered.clear();
|
||||
for (auto const index : m_indirectorder)
|
||||
{
|
||||
m_indirectdrawsordered.push_back(m_indirectdraws[index]);
|
||||
m_indirectcommandsordered.push_back(m_indirectcommands[index]);
|
||||
}
|
||||
m_indirectdraws.swap(m_indirectdrawsordered);
|
||||
m_indirectcommands.swap(m_indirectcommandsordered);
|
||||
}
|
||||
|
||||
bool scene_walk::Indirect_Mergeable(std::size_t const First, std::size_t const Other) const
|
||||
{
|
||||
auto const &first{m_indirectdraws[First]};
|
||||
auto const &other{m_indirectdraws[Other]};
|
||||
if ((first.material != other.material) || (first.blending != other.blending) || (first.type != other.type) || (first.group != other.group) ||
|
||||
(m_indirectcommands[First].indexed != m_indirectcommands[Other].indexed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (first.material == null_handle)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// the texture set the material bind binds
|
||||
auto const &material{walk_material_data(first.material)};
|
||||
if (material.texture_variants.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return (&material.GetTextures(TSubModel::VariantId(first.submodel)) == &material.GetTextures(TSubModel::VariantId(other.submodel)));
|
||||
}
|
||||
|
||||
void scene_walk::Expire_Indirect_Records()
|
||||
{
|
||||
if ((m_framestamp % 256) == 0)
|
||||
{
|
||||
// the sleeper records of tracks out of sight for a while. their places in the record buffer of the gpu culling are
|
||||
// dropped when it starts over
|
||||
for (auto records{m_sleeperrecords.begin()}; records != m_sleeperrecords.end();)
|
||||
{
|
||||
if (m_framestamp - records->second.frame > 256)
|
||||
{
|
||||
records = m_sleeperrecords.erase(records);
|
||||
}
|
||||
else
|
||||
{
|
||||
++records;
|
||||
}
|
||||
}
|
||||
// and so are the records of the models drawn on their own
|
||||
for (auto records{m_modelrecords.begin()}; records != m_modelrecords.end();)
|
||||
{
|
||||
if (m_framestamp - records->second.frame > 256)
|
||||
{
|
||||
records = m_modelrecords.erase(records);
|
||||
}
|
||||
else
|
||||
{
|
||||
++records;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void scene_walk::Clear_Indirect_Records()
|
||||
{
|
||||
m_sleeperrecords.clear();
|
||||
m_modelrecords.clear();
|
||||
m_cascaderecords.clear();
|
||||
m_walkrecords.clear();
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
273
rendering/common/shadowfit.cpp
Normal file
273
rendering/common/shadowfit.cpp
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/shadowfit.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "rendering/openglcamera.h"
|
||||
#include "utilities/Globals.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// the nearest distance the first cascade may start from and the least one the last reaches, metres: the cascades don't
|
||||
// get any closer to the camera than this, however close the pixels are
|
||||
float const SPLIT_NEAR_MIN{0.5f};
|
||||
float const SPLIT_FAR_MIN{16.f};
|
||||
// how far past its distances a cascade takes the pixels, this many metres and this share of the distance: the camera moves
|
||||
// in the frame or two before the cascades are drawn from the measurement, and a pixel which came into another cascade since
|
||||
// has to be within its map
|
||||
float const MARGIN_METRES{1.f};
|
||||
float const MARGIN_SHARE{0.05f};
|
||||
|
||||
} // namespace
|
||||
|
||||
gl::shadow_fit_data shadow_fit::begin_measurement(view const &View, unsigned int const Frame)
|
||||
{
|
||||
// numbers start from 1, 0 is no measurement
|
||||
if (++m_measurement == 0)
|
||||
++m_measurement;
|
||||
gl::shadow_fit_data data{};
|
||||
data.measurement = m_measurement;
|
||||
data.depthmin = std::numeric_limits<float>::infinity();
|
||||
for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade)
|
||||
{
|
||||
for (std::size_t axis = 0; axis < 4; ++axis)
|
||||
{
|
||||
data.bounds[cascade * 8 + axis] = 0xFFFFFFFFu;
|
||||
data.bounds[cascade * 8 + 4 + axis] = 0u;
|
||||
}
|
||||
}
|
||||
m_pending[m_measurement % PENDING] = pending{m_measurement, View, Frame};
|
||||
return data;
|
||||
}
|
||||
|
||||
glm::vec4 shadow_fit::bounds_params(view const &View)
|
||||
{
|
||||
auto const farmin{std::min(SPLIT_FAR_MIN, View.range)};
|
||||
return glm::vec4{View.range, std::min(SPLIT_NEAR_MIN, farmin * 0.25f), farmin, std::clamp(Global.gfx_shadow_sdsm_split, 0.f, 1.f)};
|
||||
}
|
||||
|
||||
glm::vec4 shadow_fit::margin_params()
|
||||
{
|
||||
return glm::vec4{MARGIN_METRES, MARGIN_SHARE, 0.f, 0.f};
|
||||
}
|
||||
|
||||
void shadow_fit::arrived(gl::shadow_fit_data const &Data)
|
||||
{
|
||||
auto const &source{m_pending[Data.measurement % PENDING]};
|
||||
if ((Data.measurement != 0) && (source.measurement == Data.measurement))
|
||||
m_latest = result{Data, source.source, source.frame};
|
||||
}
|
||||
|
||||
float shadow_fit::unorder(std::uint32_t const Value)
|
||||
{
|
||||
auto const bits{((Value & 0x80000000u) != 0u) ? (Value & 0x7FFFFFFFu) : ~Value};
|
||||
float result;
|
||||
std::memcpy(&result, &bits, sizeof(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
glm::vec3 shadow_fit::light_vector(glm::vec3 const &Sun)
|
||||
{
|
||||
// cap the vertical angle to keep shadows from getting too long
|
||||
return glm::normalize(glm::vec3{Sun.x, std::min(Sun.y, Global.gfx_shadow_angle_min), Sun.z});
|
||||
}
|
||||
|
||||
glm::mat3 shadow_fit::light_rotation(glm::vec3 const &Sun)
|
||||
{
|
||||
// the rotation of the view of the shadow pass (setup_pass())
|
||||
return glm::mat3{glm::lookAt(glm::vec3{0.f}, light_vector(Sun), glm::vec3{0.f, 1.f, 0.f})};
|
||||
}
|
||||
|
||||
std::array<float, gl::MAX_CASCADES + 1> shadow_fit::fixed_splits()
|
||||
{
|
||||
static_assert(gl::MAX_CASCADES == 3, "the fixed cascades are made for 3 of them");
|
||||
std::array<float, gl::MAX_CASCADES + 1> splits{0.0f, Global.shadowtune.range / 32, Global.shadowtune.range / 8, Global.shadowtune.range};
|
||||
if (Global.shadowtune.map_size > 2048)
|
||||
{
|
||||
// increase coverage if our shadow map is large enough to produce decent results
|
||||
for (std::size_t idx = 1; idx < splits.size(); ++idx)
|
||||
splits[idx] *= Global.shadowtune.map_size / 2048;
|
||||
}
|
||||
return splits;
|
||||
}
|
||||
|
||||
char const *shadow_fit::unavailable(bool const Postprocessing, bool const Shadow, std::size_t const Shadowviews)
|
||||
{
|
||||
if (false == Global.gfx_shadowmap_enabled)
|
||||
return "no shadow maps";
|
||||
if (Global.bWireFrame)
|
||||
return "wireframe";
|
||||
// the shadows follow the camera, the view shows another one
|
||||
if (DebugCameraFlag)
|
||||
return "debug camera";
|
||||
if (false == Postprocessing)
|
||||
return "no post processing";
|
||||
if (EditorModeFlag && Global.EditorOrtho)
|
||||
return "orthographic view";
|
||||
if ((false == Shadow) || (Shadowviews != 1))
|
||||
return "the shadows cover more than one view";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool shadow_fit::fit(unsigned int const Frame, opengl_camera const &Camera, char const *Unavailable, std::array<float, gl::MAX_CASCADES + 1> &Splits, areas &Areas, fit_state &State) const
|
||||
{
|
||||
// a measurement older than this many frames, made with the camera this many metres away or the view turned this many
|
||||
// degrees from the one now, isn't used
|
||||
constexpr unsigned int AGE_MAX{6};
|
||||
constexpr float SHIFT_MAX{25.f};
|
||||
constexpr float TURN_MAX{20.f};
|
||||
// metres an area grows by on each side: the vehicles move in the frames since the measurement too
|
||||
constexpr float MARGIN{0.25f};
|
||||
|
||||
auto &state{State};
|
||||
state = fit_state{};
|
||||
if (false == Global.gfx_shadow_sdsm)
|
||||
return false;
|
||||
if ((state.reason = Unavailable) != nullptr)
|
||||
return false;
|
||||
auto const *latest{this->latest()};
|
||||
if (latest == nullptr)
|
||||
{
|
||||
state.reason = "no measurement yet";
|
||||
return false;
|
||||
}
|
||||
auto const &data{latest->data};
|
||||
auto const &source{latest->source};
|
||||
state.age = Frame - latest->frame;
|
||||
state.depthmin = data.depthmin;
|
||||
state.depthmax = data.depthmax;
|
||||
if (state.age > AGE_MAX)
|
||||
{
|
||||
state.reason = "the measurement is too old";
|
||||
return false;
|
||||
}
|
||||
if (data.samples == 0)
|
||||
{
|
||||
state.reason = "only the sky in the view";
|
||||
return false;
|
||||
}
|
||||
if (source.range != fixed_splits().back())
|
||||
{
|
||||
state.reason = "the range of the shadows changed";
|
||||
return false;
|
||||
}
|
||||
// the shadows of this frame are made for its colour pass
|
||||
auto const &camera{Camera};
|
||||
auto const shift{glm::vec3{source.camera - camera.position()}};
|
||||
if (glm::length(shift) > SHIFT_MAX)
|
||||
{
|
||||
state.reason = "the camera jumped";
|
||||
return false;
|
||||
}
|
||||
auto const turnmatrix{glm::mat3{camera.modelview()} * glm::transpose(source.rotation)};
|
||||
auto const turn{std::acos(std::clamp((turnmatrix[0][0] + turnmatrix[1][1] + turnmatrix[2][2] - 1.f) * 0.5f, -1.f, 1.f))};
|
||||
state.turn = glm::degrees(turn);
|
||||
if (state.turn > TURN_MAX)
|
||||
{
|
||||
state.reason = "the view turned too fast";
|
||||
return false;
|
||||
}
|
||||
auto const &projection{camera.projection()};
|
||||
if ((std::abs(projection[0][0] - source.projection[0][0]) > 0.02f * std::abs(projection[0][0])) || (std::abs(projection[1][1] - source.projection[1][1]) > 0.02f * std::abs(projection[1][1])))
|
||||
{
|
||||
state.reason = "the field of view changed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// the first cascade from the camera, like the fixed ones. setup_pass() takes distances up to 1 m as shares of the far
|
||||
// end, so the others start past that
|
||||
Splits[0] = 0.f;
|
||||
for (std::size_t split = 1; split < Splits.size(); ++split)
|
||||
Splits[split] = std::max(data.splits[split], Splits[split - 1] + 1.5f);
|
||||
if (false == std::isfinite(Splits.back()))
|
||||
{
|
||||
state.reason = "invalid measurement";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const inverselight{glm::transpose(source.light)};
|
||||
// the scenery stays where it was while the camera moved, the cab and the train the camera rides with moved along. an area
|
||||
// covers both
|
||||
auto const lightshift{source.light * shift};
|
||||
auto const chord{2.f * std::sin(turn * 0.5f)};
|
||||
for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade)
|
||||
{
|
||||
// with nothing of the view in a cascade, its map covers the whole slice
|
||||
if (data.counts[cascade] == 0)
|
||||
continue;
|
||||
auto const *bounds{&data.bounds[cascade * 8]};
|
||||
glm::vec3 low{unorder(bounds[0]), unorder(bounds[1]), unorder(bounds[2])};
|
||||
glm::vec3 high{unorder(bounds[4]), unorder(bounds[5]), unorder(bounds[6])};
|
||||
if ((false == std::isfinite(low.x + low.y + high.x + high.y)) || (low.x > high.x) || (low.y > high.y))
|
||||
continue;
|
||||
low = glm::min(low, low + lightshift);
|
||||
high = glm::max(high, high + lightshift);
|
||||
// and what came into the view as it turned, out to the reach of the cascade
|
||||
auto const reach{Splits[cascade + 1] + static_cast<float>(gl::MAX_CASCADES - cascade)};
|
||||
auto const margin{reach * chord + MARGIN + 0.01f * std::max(high.x - low.x, high.y - low.y)};
|
||||
low -= glm::vec3{margin};
|
||||
high += glm::vec3{margin};
|
||||
auto &area{Areas[cascade].emplace()};
|
||||
for (int corner = 0; corner < 8; ++corner)
|
||||
area[corner] = inverselight * glm::vec3{(corner & 1) ? high.x : low.x, (corner & 2) ? high.y : low.y, (corner & 4) ? high.z : low.z};
|
||||
}
|
||||
state.fitted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
float shadow_fit::fit_slice(std::vector<glm::vec4> &Points, std::array<glm::vec3, 8> const *Area, glm::mat3 const &Lightrotation, float &Slicesize)
|
||||
{
|
||||
// a cascade fitted to the depth of the view (gfx.shadow.sdsm) covers the area of the pixels it takes, within the slice of the
|
||||
// view. the areas are compared across the light, the depth along it stays the one of the slice
|
||||
auto quantizationstep{0.f};
|
||||
glm::vec3 slicemin{std::numeric_limits<float>::max()}, slicemax{std::numeric_limits<float>::lowest()};
|
||||
for (auto const &point : Points)
|
||||
{
|
||||
auto const lightpoint{Lightrotation * glm::vec3{point}};
|
||||
slicemin = glm::min(slicemin, lightpoint);
|
||||
slicemax = glm::max(slicemax, lightpoint);
|
||||
}
|
||||
Slicesize = std::max(slicemax.x - slicemin.x, slicemax.y - slicemin.y);
|
||||
if (Area != nullptr)
|
||||
{
|
||||
glm::vec3 areamin{std::numeric_limits<float>::max()}, areamax{std::numeric_limits<float>::lowest()};
|
||||
for (auto const &point : *Area)
|
||||
{
|
||||
auto const lightpoint{Lightrotation * point};
|
||||
areamin = glm::min(areamin, lightpoint);
|
||||
areamax = glm::max(areamax, lightpoint);
|
||||
}
|
||||
auto const fitmin{glm::vec2{glm::max(glm::vec2{slicemin}, glm::vec2{areamin})}};
|
||||
auto const fitmax{glm::vec2{glm::min(glm::vec2{slicemax}, glm::vec2{areamax})}};
|
||||
if ((fitmin.x < fitmax.x) && (fitmin.y < fitmax.y))
|
||||
{
|
||||
// the size changes in steps of a power of two of about 1/16 of it, so that the texels of the map keep their size until
|
||||
// the area grows or shrinks by a step
|
||||
auto const size{std::max(fitmax.x - fitmin.x, fitmax.y - fitmin.y)};
|
||||
quantizationstep = std::exp2(std::ceil(std::log2(std::max(size, 1.f) / 16.f)));
|
||||
Points.clear();
|
||||
auto const inverserotation{glm::transpose(Lightrotation)};
|
||||
for (int corner = 0; corner < 8; ++corner)
|
||||
{
|
||||
auto const lightpoint{glm::vec3{(corner & 1) ? fitmax.x : fitmin.x, (corner & 2) ? fitmax.y : fitmin.y, (corner & 4) ? slicemax.z : slicemin.z}};
|
||||
Points.emplace_back(inverserotation * lightpoint, 1.f);
|
||||
}
|
||||
}
|
||||
}
|
||||
return quantizationstep;
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
116
rendering/common/shadowfit.h
Normal file
116
rendering/common/shadowfit.h
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "gl/ubo.h"
|
||||
|
||||
class opengl_camera;
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
// the cpu side of the cascades of the sun shadows fitted to the depth of the main view (sample distribution shadow maps,
|
||||
// gfx.shadow.sdsm), shared by the renderers (opengl46_shadow_fit, the vulkan renderer). at the end of the colour pass of the main
|
||||
// view two compute passes go over its resolved depth: the nearest and the farthest distance of the pixels which aren't the sky
|
||||
// (shadow_fit_depth.comp), then the distances split between the cascades and, for each cascade, the box of the pixels it takes
|
||||
// in the space of the light (shadow_fit_bounds.comp). the result comes back to the cpu a frame or two later without waiting,
|
||||
// with the camera and the light it was measured with, and the cascades of the frame cover the boxes (fit()) instead of the
|
||||
// whole slices of the view between the fixed distances of shadowtune
|
||||
class shadow_fit
|
||||
{
|
||||
public:
|
||||
// the view and the light a measurement was made with
|
||||
struct view
|
||||
{
|
||||
glm::dvec3 camera; // position of the camera
|
||||
glm::mat4 projection; // projection of the view
|
||||
glm::mat3 rotation; // world to view space, rotation only
|
||||
glm::mat3 light; // world to the space of the light, rotation only
|
||||
float range{0.f}; // the farthest distance the cascades may reach
|
||||
};
|
||||
// a measurement which came back
|
||||
struct result
|
||||
{
|
||||
gl::shadow_fit_data data;
|
||||
view source;
|
||||
unsigned int frame{0}; // frame of the renderer it was made in
|
||||
};
|
||||
// what the last shadow pass did, for the debug panel
|
||||
struct fit_state
|
||||
{
|
||||
bool fitted{false};
|
||||
char const *reason{nullptr}; // why not, with gfx.shadow.sdsm on
|
||||
unsigned int age{0}; // frames since the measurement
|
||||
float depthmin{0.f};
|
||||
float depthmax{0.f};
|
||||
float turn{0.f}; // of the view since the measurement, degrees
|
||||
std::array<float, gl::MAX_CASCADES + 1> splits{};
|
||||
// size of the area of the map of each cascade: of the whole slice of the view, and as drawn; metres
|
||||
std::array<float, gl::MAX_CASCADES> slice{};
|
||||
std::array<float, gl::MAX_CASCADES> drawn{};
|
||||
};
|
||||
using areas = std::array<std::optional<std::array<glm::vec3, 8>>, gl::MAX_CASCADES>;
|
||||
|
||||
// a measurement made: its number in the data the passes start from (no pixels, the nearest distance infinite, the bounds
|
||||
// empty), kept with the view until it comes back
|
||||
gl::shadow_fit_data begin_measurement(view const &View, unsigned int const Frame);
|
||||
// the parameters of shadow_fit_bounds.comp: the range, the nearest end of the first split, the least far end of the last
|
||||
// one, the split between the logarithmic and the even splits; and the margins of the bounds
|
||||
static glm::vec4 bounds_params(view const &View);
|
||||
static glm::vec4 margin_params();
|
||||
// a measurement which came back from the gpu
|
||||
void arrived(gl::shadow_fit_data const &Data);
|
||||
// the newest measurement taken in so far
|
||||
result const *latest() const
|
||||
{
|
||||
return m_latest ? &(*m_latest) : nullptr;
|
||||
}
|
||||
// the ends of the bounds, as the passes store them (shadow_fit_order() in shadow_fit_bounds.comp)
|
||||
static float unorder(std::uint32_t const Value);
|
||||
|
||||
// the cascades of the frame fitted to the latest measurement: the distances they go from and to, and for each cascade the
|
||||
// corners of the area its map is to cover, camera-relative world space (none: the whole slice of the view). false: the fixed
|
||||
// cascades. Camera: of the colour pass the shadows are made for; Unavailable: why the cascades can't be fitted, or null
|
||||
bool fit(unsigned int const Frame, opengl_camera const &Camera, char const *Unavailable, std::array<float, gl::MAX_CASCADES + 1> &Splits, areas &Areas, fit_state &State) const;
|
||||
|
||||
// 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
|
||||
static glm::vec3 light_vector(glm::vec3 const &Sun);
|
||||
static glm::mat3 light_rotation(glm::vec3 const &Sun);
|
||||
// the distances the fixed cascades of the sun shadows go from and to (shadowtune)
|
||||
static std::array<float, gl::MAX_CASCADES + 1> fixed_splits();
|
||||
// why the cascades can't be fitted to the depth of a viewport, null if they can. Postprocessing: the viewport has the images
|
||||
// of the post-processing (the resolved depth); Shadow: its view is taken into the shadows; Shadowviews: the viewports which are
|
||||
static char const *unavailable(bool const Postprocessing, bool const Shadow, std::size_t const Shadowviews);
|
||||
// the points of the slice of the view a shadow pass covers (setup_pass()), in its place the corners of the area fitted to the
|
||||
// depth, when there is one (and it lies within the slice); Slicesize: the size of the slice across the light. returns the step
|
||||
// the bounds of the map are quantized to, 0 when the area isn't fitted
|
||||
static float fit_slice(std::vector<glm::vec4> &Points, std::array<glm::vec3, 8> const *Area, glm::mat3 const &Lightrotation, float &Slicesize);
|
||||
|
||||
private:
|
||||
// a measurement on its way back, kept by its number
|
||||
struct pending
|
||||
{
|
||||
std::uint32_t measurement{0}; // 0: none
|
||||
view source;
|
||||
unsigned int frame{0};
|
||||
};
|
||||
static constexpr std::size_t PENDING = 8; // more than the copies the readbacks keep in flight
|
||||
|
||||
std::array<pending, PENDING> m_pending;
|
||||
std::uint32_t m_measurement{0}; // number of the last measurement
|
||||
std::optional<result> m_latest;
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
289
rendering/common/skylight.cpp
Normal file
289
rendering/common/skylight.cpp
Normal file
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/skylight.h"
|
||||
|
||||
#include "environment/skydome.h"
|
||||
#include "utilities/Globals.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// irradiance of a radiance given as spherical harmonics, divided by pi (the units of the ambient light of the shading:
|
||||
// under a uniform sky of radiance L it is L): the coefficients of band l are scaled by A_l / pi (Ramamoorthi, Hanrahan 2001)
|
||||
float const SH_IRRADIANCE_BAND[3]{1.f, 2.f / 3.f, 0.25f};
|
||||
int const SH_BAND[gl::SKY_SH_COEFFICIENTS]{0, 1, 1, 1, 2, 2, 2, 2, 2};
|
||||
float const SH_Y00{0.282095f};
|
||||
float const SH_Y1{0.488603f};
|
||||
float const PI{3.14159265359f};
|
||||
|
||||
// real spherical harmonics of order 2 in a world space direction, in the order of light_common.glsl
|
||||
std::array<float, gl::SKY_SH_COEFFICIENTS> sh_basis(glm::vec3 const &n)
|
||||
{
|
||||
return {SH_Y00,
|
||||
SH_Y1 * n.y,
|
||||
SH_Y1 * n.z,
|
||||
SH_Y1 * n.x,
|
||||
1.092548f * n.x * n.y,
|
||||
1.092548f * n.y * n.z,
|
||||
0.315392f * (3.f * n.z * n.z - 1.f),
|
||||
1.092548f * n.x * n.z,
|
||||
0.546274f * (n.x * n.x - n.y * n.y)};
|
||||
}
|
||||
|
||||
float luminance(glm::vec3 const &Color)
|
||||
{
|
||||
return glm::dot(Color, glm::vec3{0.2126f, 0.7152f, 0.0722f});
|
||||
}
|
||||
|
||||
glm::vec3 desaturate(glm::vec3 const &Color, float const Amount)
|
||||
{
|
||||
return glm::mix(Color, glm::vec3{luminance(Color)}, Amount);
|
||||
}
|
||||
|
||||
// the response of color.frag to the vertex colours of the dome, so the light taken from the dome is the light it shows
|
||||
glm::vec3 sky_shoulder(glm::vec3 const &Color)
|
||||
{
|
||||
float const knee{0.25f};
|
||||
float const range{0.95f - knee};
|
||||
auto const excess{glm::max(Color - glm::vec3{knee}, glm::vec3{0.f})};
|
||||
return glm::min(Color, glm::vec3{knee}) + range * (glm::vec3{1.f} - glm::exp(-excess / range));
|
||||
}
|
||||
|
||||
// the atmosphere is calibrated against the old sky: these scales make a sky of the atmosphere, with the sun 15 to 60 degrees
|
||||
// high on a clear day, as bright as the old Perez dome is drawn on average, and the ambient light and the sunlight as strong
|
||||
// as the old ones, with the default ground albedo (measured on the tables worked out by the same shaders on a software
|
||||
// renderer, see the notes of the plan, stage S2)
|
||||
float const ATMOSPHERE_SKY_SCALE{16.f};
|
||||
float const ATMOSPHERE_AMBIENT_SCALE{44.f};
|
||||
float const ATMOSPHERE_SUN_SCALE{1.45f};
|
||||
// sun elevations (degrees) between which the sky goes over from the old model (night, twilight) to the atmosphere. the
|
||||
// atmosphere darkens at dusk much faster than the old model, which the rest of the lighting was tuned to
|
||||
float const ATMOSPHERE_FADE_START{-6.f};
|
||||
float const ATMOSPHERE_FADE_END{10.f};
|
||||
// the shading scales the sunlight and the ambient light by these (light_common.glsl), so the light the sun gives the
|
||||
// ground, in the units of the ambient light, is the diffuse colour times their ratio
|
||||
float const SHADING_SUN_TO_AMBIENT{1.5f / 0.65f};
|
||||
|
||||
// parameters of the earth's atmosphere after Hillaire 2020 (km, 1/km)
|
||||
glm::vec3 const RAYLEIGH_SCATTERING{5.802e-3f, 13.558e-3f, 33.1e-3f};
|
||||
float const RAYLEIGH_SCALE_HEIGHT{8.f};
|
||||
float const MIE_SCATTERING{3.996e-3f};
|
||||
float const MIE_EXTINCTION{4.440e-3f};
|
||||
float const MIE_SCALE_HEIGHT{1.2f};
|
||||
float const MIE_ASYMMETRY{0.8f};
|
||||
glm::vec3 const OZONE_ABSORPTION{0.650e-3f, 1.881e-3f, 0.085e-3f};
|
||||
float const OZONE_ALTITUDE{25.f};
|
||||
float const OZONE_HALF_WIDTH{15.f};
|
||||
float const BOTTOM_RADIUS{6360.f};
|
||||
float const TOP_RADIUS{6460.f};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
sky_light::sky_light()
|
||||
{
|
||||
// a valid atmosphere for the sky shader from the start, the tables stay black until the first update
|
||||
m_atmosphere.rayleigh = glm::vec4{RAYLEIGH_SCATTERING, RAYLEIGH_SCALE_HEIGHT};
|
||||
m_atmosphere.mie = glm::vec4{MIE_SCATTERING, MIE_EXTINCTION, MIE_SCALE_HEIGHT, MIE_ASYMMETRY};
|
||||
m_atmosphere.ozone = glm::vec4{OZONE_ABSORPTION, OZONE_ALTITUDE};
|
||||
m_atmosphere.ground = glm::vec4{glm::vec3{Global.gfx_ambient_groundalbedo}, OZONE_HALF_WIDTH};
|
||||
m_atmosphere.sun_direction = glm::vec4{0.f, 1.f, 0.f, 0.f};
|
||||
m_atmosphere.bottom_radius = BOTTOM_RADIUS;
|
||||
m_atmosphere.top_radius = TOP_RADIUS;
|
||||
m_atmosphere.observer_radius = BOTTOM_RADIUS + 0.2f;
|
||||
}
|
||||
|
||||
sky_light::sh_coefficients sky_light::dome_directional(CSkyDome const &Skydome, glm::vec3 const &Ambient, glm::vec3 const &Sunlight, float const Sunheight) const
|
||||
{
|
||||
sh_coefficients radiance{};
|
||||
auto const &vertices{Skydome.vertices()};
|
||||
auto const &colors{Skydome.colors()};
|
||||
// the vertices make a grid even in the elevation and the azimuth, so the solid angle of a vertex goes with the cosine of
|
||||
// its elevation. the bottom ring lies a little under the horizon and is taken as the horizon
|
||||
float totalweight{0.f};
|
||||
for (std::size_t i = 0; i < vertices.size() && i < colors.size(); ++i)
|
||||
{
|
||||
auto direction{glm::normalize(vertices[i])};
|
||||
// the dome darkens its colours towards the top and brightens the band above the horizon for the looks
|
||||
// (CSkyDome::RebuildColors), which would make the zenith almost black and the horizon the main source of light.
|
||||
// both are undone here, so the light comes from the sky the Perez model describes
|
||||
auto const height{std::max(direction.y, 0.05f)};
|
||||
auto const horizonband{std::clamp(1.f - height / 0.2f, 0.f, 1.f)};
|
||||
auto const looks{std::clamp(1.f - height * 0.75f, 0.25f, 1.f) * std::lerp(1.f, 1.5f + 0.75f * m_overcast, horizonband)};
|
||||
direction.y = std::max(direction.y, 0.f);
|
||||
direction = glm::normalize(direction);
|
||||
auto const weight{std::sqrt(std::max(1.f - direction.y * direction.y, 0.f))};
|
||||
auto const color{sky_shoulder(glm::pow(glm::max(colors[i] / looks, glm::vec3{0.f}), glm::vec3{2.2f}))};
|
||||
auto const basis{sh_basis(direction)};
|
||||
for (std::size_t c = 0; c < radiance.size(); ++c)
|
||||
radiance[c] += color * (basis[c] * weight);
|
||||
totalweight += weight;
|
||||
}
|
||||
if (totalweight <= 0.f)
|
||||
return {};
|
||||
for (auto &coefficient : radiance)
|
||||
coefficient *= 2.f * PI / totalweight;
|
||||
|
||||
// the ground, lit by the sky and the sun, fills the lower hemisphere with uniform light (see atmosphere_ambient.comp).
|
||||
// the dome is in its own units: the light of the sky on an upward facing surface is taken to be the ambient light,
|
||||
// which converts the sunlight to them
|
||||
auto const up{sh_basis(glm::vec3{0.f, 1.f, 0.f})};
|
||||
glm::vec3 skyup{0.f};
|
||||
for (std::size_t c = 0; c < radiance.size(); ++c)
|
||||
skyup += radiance[c] * (SH_IRRADIANCE_BAND[SH_BAND[c]] * up[c]);
|
||||
auto const tosky{luminance(skyup) / std::max(luminance(Ambient), 1e-4f)};
|
||||
auto const groundradiance{Global.gfx_ambient_groundalbedo * (skyup + Sunlight * SHADING_SUN_TO_AMBIENT * std::max(Sunheight, 0.f) * tosky)};
|
||||
radiance[0] += groundradiance * (SH_Y00 * 2.f * PI);
|
||||
radiance[1] += groundradiance * (SH_Y1 * -PI);
|
||||
|
||||
// to the irradiance, scaled so its average over all directions is the ambient light, channel by channel
|
||||
sh_coefficients directional{};
|
||||
glm::vec3 const average{radiance[0] * SH_Y00};
|
||||
auto const scale{Ambient / glm::max(average, glm::vec3{1e-4f})};
|
||||
for (std::size_t c = 1; c < radiance.size(); ++c)
|
||||
directional[c] = radiance[c] * SH_IRRADIANCE_BAND[SH_BAND[c]] * scale;
|
||||
return directional;
|
||||
}
|
||||
|
||||
void sky_light::begin_update()
|
||||
{
|
||||
m_overcast = std::clamp(Global.Overcast, 0.f, 1.f);
|
||||
m_weight = 0.f;
|
||||
}
|
||||
|
||||
bool sky_light::atmosphere_changed(glm::vec3 const &Sundirection, bool &Medium)
|
||||
{
|
||||
// aerosols grow with the turbidity the weather sets (3 clear .. 9 overcast)
|
||||
auto const turbidity{std::clamp(Global.fTurbidity, 3.f, 9.f)};
|
||||
auto const miescale{Global.gfx_atmosphere_haze * (1.f + 0.5f * (turbidity - 3.f))};
|
||||
glm::vec4 const medium{miescale, Global.gfx_ambient_groundalbedo, Global.gfx_atmosphere_altitude, 0.f};
|
||||
auto const mediumchanged{!m_tables_medium || *m_tables_medium != medium};
|
||||
// the sky-view table follows the sun when it moved by more than about a twentieth of a degree
|
||||
auto const sunchanged{!m_tables_sun || glm::dot(*m_tables_sun, Sundirection) < 0.9999995f};
|
||||
Medium = mediumchanged;
|
||||
if (!mediumchanged && !sunchanged)
|
||||
return false;
|
||||
|
||||
m_atmosphere.mie = glm::vec4{MIE_SCATTERING * miescale, MIE_EXTINCTION * miescale, MIE_SCALE_HEIGHT, MIE_ASYMMETRY};
|
||||
m_atmosphere.ground = glm::vec4{glm::vec3{Global.gfx_ambient_groundalbedo}, OZONE_HALF_WIDTH};
|
||||
m_atmosphere.sun_direction = glm::vec4{Sundirection, 0.f};
|
||||
m_atmosphere.observer_radius = BOTTOM_RADIUS + std::max(Global.gfx_atmosphere_altitude, 0.f) * 0.001f + 0.001f;
|
||||
m_tables_medium = medium;
|
||||
m_tables_sun = Sundirection;
|
||||
return true;
|
||||
}
|
||||
|
||||
void sky_light::end_update(basic_light &Sunlight, glm::vec3 &Fogcolor, CSkyDome const &Skydome, glm::vec3 const &Sundirection, float const Sunangle)
|
||||
{
|
||||
// the light the simulation set, which is kept where the atmosphere is not used
|
||||
glm::vec3 const oldambient{Sunlight.ambient};
|
||||
glm::vec3 const olddiffuse{Sunlight.diffuse};
|
||||
glm::vec3 const oldfog{Fogcolor};
|
||||
|
||||
if (Global.gfx_atmosphere && m_result)
|
||||
m_weight = glm::smoothstep(ATMOSPHERE_FADE_START, ATMOSPHERE_FADE_END, Sunangle);
|
||||
|
||||
sh_coefficients directional{};
|
||||
if (Global.gfx_ambient_sh)
|
||||
{
|
||||
// the dome changes with the sun, the weather and the light the simulation works out from them
|
||||
std::array<glm::vec4, 3> const key{glm::vec4{Sundirection, Sunlight.direction.y}, glm::vec4{oldambient, m_overcast}, glm::vec4{olddiffuse, Global.gfx_ambient_groundalbedo}};
|
||||
if (!m_dome_key || *m_dome_key != key)
|
||||
{
|
||||
m_dome_sh = dome_directional(Skydome, oldambient, olddiffuse, -Sunlight.direction.y);
|
||||
m_dome_key = key;
|
||||
}
|
||||
directional = m_dome_sh;
|
||||
}
|
||||
|
||||
m_sunlight = olddiffuse;
|
||||
m_ambient = oldambient;
|
||||
m_fog = oldfog;
|
||||
m_sun_disc_color = glm::vec3{1.f};
|
||||
if (m_weight > 0.f)
|
||||
{
|
||||
auto const &result{*m_result};
|
||||
// the camera's white balance: the sun high in the sky is white
|
||||
auto const balance{1.f / glm::max(glm::vec3{result.zenith_transmittance}, glm::vec3{1e-3f})};
|
||||
m_white_balance = balance / std::max(luminance(balance), 1e-4f);
|
||||
|
||||
// the atmosphere has no clouds: the overcast takes away the sunlight, greys the sky and evens out its light
|
||||
// with the shadows of the clouds the sun is held back where they are, not everywhere
|
||||
auto const overcastsun{m_cloud_shadows ? 1.f : (1.f - 0.65f * m_overcast)};
|
||||
auto const sun{glm::vec3{result.sun_transmittance} * m_white_balance * (ATMOSPHERE_SUN_SCALE * Global.gfx_atmosphere_sun) * overcastsun};
|
||||
auto const ambientscale{ATMOSPHERE_AMBIENT_SCALE * Global.gfx_atmosphere_ambient};
|
||||
auto const ambient{desaturate(glm::vec3{result.radiance_sh[0]} * SH_Y00 * m_white_balance * ambientscale, 0.5f * m_overcast)};
|
||||
m_sky_scale = ATMOSPHERE_SKY_SCALE * Global.gfx_atmosphere_sky;
|
||||
auto const fog{desaturate(glm::vec3{result.horizon} * m_white_balance * m_sky_scale, 0.5f * m_overcast)};
|
||||
m_sky_flat_luminance = luminance(glm::vec3{result.average} * m_white_balance * m_sky_scale);
|
||||
|
||||
m_sunlight = glm::mix(olddiffuse, sun, m_weight);
|
||||
m_ambient = glm::mix(oldambient, ambient, m_weight);
|
||||
m_fog = glm::mix(oldfog, fog, m_weight);
|
||||
auto const sundisc{glm::vec3{result.sun_transmittance} * m_white_balance};
|
||||
m_sun_disc_color = sundisc / std::max({sundisc.r, sundisc.g, sundisc.b, 1e-4f});
|
||||
|
||||
if (Global.gfx_ambient_sh)
|
||||
{
|
||||
auto const evenness{1.f - 0.75f * m_overcast};
|
||||
for (std::size_t c = 1; c < directional.size(); ++c)
|
||||
{
|
||||
auto const atmosphere{desaturate(glm::vec3{result.radiance_sh[c]} * m_white_balance * (SH_IRRADIANCE_BAND[SH_BAND[c]] * ambientscale * evenness), 0.5f * m_overcast)};
|
||||
directional[c] = glm::mix(directional[c], atmosphere, m_weight);
|
||||
}
|
||||
}
|
||||
|
||||
Sunlight.diffuse = glm::vec4{m_sunlight, Sunlight.diffuse.a};
|
||||
Sunlight.ambient = glm::vec4{m_ambient, Sunlight.ambient.a};
|
||||
Fogcolor = m_fog;
|
||||
}
|
||||
|
||||
// directional part of the ambient light for the shading
|
||||
gl::sky_data skydata{};
|
||||
for (std::size_t c = 1; c < directional.size(); ++c)
|
||||
skydata.ambient_sh[c] = glm::vec4{directional[c] * Global.gfx_ambient_sh_strength, 0.f};
|
||||
m_sky_data = skydata;
|
||||
}
|
||||
|
||||
void sky_light::sky_params(glm::vec4 &Param0, glm::vec4 &Param1) const
|
||||
{
|
||||
// the volumetric clouds make the overcast sky themselves
|
||||
Param0 = glm::vec4{m_weight, m_sky_scale, m_clouds ? 0.f : 0.75f * m_overcast, m_sky_flat_luminance};
|
||||
Param1 = glm::vec4{m_white_balance, 0.f};
|
||||
}
|
||||
|
||||
glm::vec3 sky_light::sun_disc_color(glm::vec3 const &Oldcolor) const
|
||||
{
|
||||
return glm::mix(Oldcolor, m_sun_disc_color, m_weight);
|
||||
}
|
||||
|
||||
void sky_light::draw_debug_ui() const
|
||||
{
|
||||
ImGui::Checkbox("directional ambient light (gfx.ambient.sh)", &Global.gfx_ambient_sh);
|
||||
ImGui::SliderFloat("directional strength", &Global.gfx_ambient_sh_strength, 0.f, 2.f);
|
||||
ImGui::SliderFloat("ground albedo", &Global.gfx_ambient_groundalbedo, 0.f, 1.f);
|
||||
ImGui::Checkbox("GGX prefiltered reflections (gfx.envmap.ggx)", &Global.gfx_envmap_ggx);
|
||||
ImGui::Checkbox("physical atmosphere (gfx.atmosphere)", &Global.gfx_atmosphere);
|
||||
ImGui::SliderFloat("sky", &Global.gfx_atmosphere_sky, 0.f, 4.f);
|
||||
ImGui::SliderFloat("ambient", &Global.gfx_atmosphere_ambient, 0.f, 4.f);
|
||||
ImGui::SliderFloat("sun", &Global.gfx_atmosphere_sun, 0.f, 4.f);
|
||||
ImGui::SliderFloat("haze", &Global.gfx_atmosphere_haze, 0.f, 8.f);
|
||||
ImGui::SliderFloat("altitude (m)", &Global.gfx_atmosphere_altitude, 0.f, 3000.f);
|
||||
ImGui::Text("atmosphere share %.2f%s", m_weight, (Global.gfx_atmosphere && !m_result) ? " (waiting for the first read back)" : "");
|
||||
ImGui::Text("sun %.3f %.3f %.3f", m_sunlight.r, m_sunlight.g, m_sunlight.b);
|
||||
ImGui::Text("ambient %.3f %.3f %.3f", m_ambient.r, m_ambient.g, m_ambient.b);
|
||||
ImGui::Text("fog %.3f %.3f %.3f", m_fog.r, m_fog.g, m_fog.b);
|
||||
ImGui::Text("up / down ambient %.3f / %.3f", luminance(m_ambient + glm::vec3{m_sky_data.ambient_sh[1]} * SH_Y1), luminance(m_ambient - glm::vec3{m_sky_data.ambient_sh[1]} * SH_Y1));
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
131
rendering/common/skylight.h
Normal file
131
rendering/common/skylight.h
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
// the light of the sky the renderers share (the part of the stage S of claude/gl46-renderer-features-plan.md which isn't the
|
||||
// work of the gpu, taken out of opengl46_sky for the vulkan renderer, stage P1 of claude/vulkan14-renderer-plan.md):
|
||||
// - the directional part of the ambient light, as spherical harmonics (gl::sky_data), worked out from the colours of the sky
|
||||
// dome or from the physical atmosphere
|
||||
// - the parameters of the physical atmosphere (gl::atmosphere_ubs) and when its look-up tables are to be made again
|
||||
// - what the result of the atmosphere read back from the gpu (gl::atmosphere_result) makes of the sunlight, the ambient light
|
||||
// and the fog colour of the frame, the white balance, the share of the atmosphere in the sky and the colour of the sun disc
|
||||
// switched by Global.gfx_ambient_sh and gfx_atmosphere; off, the light the simulation set is left as it is
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
|
||||
#include "gl/ubo.h"
|
||||
#include "rendering/light.h"
|
||||
|
||||
class CSkyDome;
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
class sky_light
|
||||
{
|
||||
public:
|
||||
// the parameters of the atmosphere valid from the start, the tables stay black until they are first made
|
||||
sky_light();
|
||||
|
||||
// the first part of the update of a frame: the overcast of the frame, no atmosphere yet
|
||||
void begin_update();
|
||||
// the inputs of the look-up tables of the atmosphere for the frame (Sundirection normalized): true when the tables are to be
|
||||
// made now, Medium when the transmittance and multiple scattering ones are too (the sky-view one and the ambient light follow
|
||||
// the sun). the parameters are set up for them, and the tables are taken as made
|
||||
bool atmosphere_changed(glm::vec3 const &Sundirection, bool &Medium);
|
||||
// a result of the atmosphere read back from the gpu
|
||||
void set_result(gl::atmosphere_result const &Result)
|
||||
{
|
||||
m_result = Result;
|
||||
}
|
||||
// the rest of the update: replaces the colours of Sunlight (diffuse, ambient) and Fogcolor, which come in as the simulation
|
||||
// set them, with the light of the sky, and works out the directional part of the ambient light. Sundirection and Sunangle
|
||||
// (degrees) are those of the sun, also when the moon is the key light
|
||||
void end_update(basic_light &Sunlight, glm::vec3 &Fogcolor, CSkyDome const &Skydome, glm::vec3 const &Sundirection, float const Sunangle);
|
||||
|
||||
gl::atmosphere_ubs const &atmosphere() const
|
||||
{
|
||||
return m_atmosphere;
|
||||
}
|
||||
// the directional part of the ambient light for the shading
|
||||
gl::sky_data const &sky_data() const
|
||||
{
|
||||
return m_sky_data;
|
||||
}
|
||||
// share of the atmosphere in the sky of the frame; 0 when the old sky is drawn alone
|
||||
float atmosphere_weight() const
|
||||
{
|
||||
return m_weight;
|
||||
}
|
||||
bool has_result() const
|
||||
{
|
||||
return m_result.has_value();
|
||||
}
|
||||
glm::vec3 const &white_balance() const
|
||||
{
|
||||
return m_white_balance;
|
||||
}
|
||||
float sky_scale() const
|
||||
{
|
||||
return m_sky_scale;
|
||||
}
|
||||
float overcast() const
|
||||
{
|
||||
return m_overcast;
|
||||
}
|
||||
// the volumetric clouds are drawn over the sky (it doesn't take the overcast look then), and their shadows hold back the
|
||||
// sunlight (it isn't dimmed by the overcast then)
|
||||
void set_clouds(bool const Clouds, bool const Shadows)
|
||||
{
|
||||
m_clouds = Clouds;
|
||||
m_cloud_shadows = Shadows;
|
||||
}
|
||||
// the shader parameters of the sky drawn with the atmosphere (sky_atmosphere.frag), param[0] and param[1]
|
||||
void sky_params(glm::vec4 &Param0, glm::vec4 &Param1) const;
|
||||
// colour of the sun disc, the old one blended with the colour the atmosphere gives it
|
||||
glm::vec3 sun_disc_color(glm::vec3 const &Oldcolor) const;
|
||||
// controls of the debug window
|
||||
void draw_debug_ui() const;
|
||||
|
||||
private:
|
||||
// spherical harmonics of order 2, rgb
|
||||
using sh_coefficients = std::array<glm::vec3, gl::SKY_SH_COEFFICIENTS>;
|
||||
|
||||
// directional part of the ambient light from the colours of the dome, scaled so its average is Ambient
|
||||
sh_coefficients dome_directional(CSkyDome const &Skydome, glm::vec3 const &Ambient, glm::vec3 const &Sunlight, float const Sunheight) const;
|
||||
|
||||
gl::atmosphere_ubs m_atmosphere{};
|
||||
// inputs the tables were last made for: the transmittance and multiple scattering tables depend on the medium only,
|
||||
// the sky-view table also on the sun
|
||||
std::optional<glm::vec4> m_tables_medium;
|
||||
std::optional<glm::vec3> m_tables_sun;
|
||||
std::optional<gl::atmosphere_result> m_result;
|
||||
gl::sky_data m_sky_data{};
|
||||
|
||||
// what the update made of the frame, for the sky, the sun disc and the debug window
|
||||
float m_weight{0.f};
|
||||
glm::vec3 m_white_balance{1.f};
|
||||
float m_sky_scale{0.f};
|
||||
float m_overcast{0.f};
|
||||
bool m_clouds{false};
|
||||
bool m_cloud_shadows{false};
|
||||
float m_sky_flat_luminance{0.f};
|
||||
glm::vec3 m_sun_disc_color{1.f};
|
||||
glm::vec3 m_sunlight{0.f};
|
||||
glm::vec3 m_ambient{0.f};
|
||||
glm::vec3 m_fog{0.f};
|
||||
|
||||
// cache of the directional ambient light of the dome, remade when its inputs change
|
||||
std::optional<std::array<glm::vec4, 3>> m_dome_key;
|
||||
sh_coefficients m_dome_sh{};
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
373
rendering/common/terrainmaterials.cpp
Normal file
373
rendering/common/terrainmaterials.cpp
Normal file
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/terrainmaterials.h"
|
||||
|
||||
#include "rendering/common/shadersource.h"
|
||||
#include "utilities/Logs.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int constexpr initial_capacity{8};
|
||||
int constexpr copy_attempts{600}; // frames to wait for the textures of a layer to load before it's left with what it has
|
||||
float constexpr default_repeat{8.f};
|
||||
|
||||
// array textures: diffuse, normal maps, specular/gloss maps
|
||||
std::array<int, 3> constexpr array_sizes{1024, 512, 256};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool terrain_materials::init()
|
||||
{
|
||||
if (m_state != 0)
|
||||
return m_state > 0;
|
||||
m_state = -1;
|
||||
if (false == gpu_init())
|
||||
return false;
|
||||
|
||||
m_white = make_texture("internal_src:terrain/white", texture_kind::white);
|
||||
gpu_white(m_textures.texture(m_white));
|
||||
char const *names[]{"internal_src:terrain/layers_diffuse", "internal_src:terrain/layers_normal", "internal_src:terrain/layers_specgloss"};
|
||||
for (std::size_t i = 0; i < m_arrays.size(); ++i)
|
||||
{
|
||||
auto &array{m_arrays[i]};
|
||||
array.handle = make_texture(names[i], texture_kind::layers);
|
||||
array.size = array_sizes[i];
|
||||
array.srgb = (i == 0);
|
||||
gpu_storage(array, initial_capacity, false);
|
||||
}
|
||||
m_capacity = initial_capacity;
|
||||
gpu_changed();
|
||||
|
||||
// the shader is checked now, so a broken one makes the terrain fall back on plain materials instead of the failure indicator
|
||||
try
|
||||
{
|
||||
m_program = GfxRenderer->Fetch_Shader("terrain");
|
||||
}
|
||||
catch (gfx::shader_exception const &e)
|
||||
{
|
||||
ErrorLog("heightmap terrain: can't use the terrain shader: " + std::string(e.what()));
|
||||
return false;
|
||||
}
|
||||
m_state = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
texture_handle terrain_materials::make_texture(std::string const &Name, texture_kind const Kind)
|
||||
{
|
||||
auto const handle{m_textures.create(Name, false, GL_RGBA)};
|
||||
auto &texture{m_textures.texture(handle)};
|
||||
gpu_texture(texture, Kind);
|
||||
// the content is supplied here, the texture manager isn't to load, release or replace it
|
||||
texture.is_ready = true;
|
||||
texture.is_static = true;
|
||||
texture.is_rendertarget = true;
|
||||
texture.is_texstub = false;
|
||||
texture.data_state = resource_state::good;
|
||||
return handle;
|
||||
}
|
||||
|
||||
void terrain_materials::read_material(layer &Layer)
|
||||
{
|
||||
auto const &material{m_materials.material(Layer.material)};
|
||||
Layer.repeat = (Layer.size > 0.f ? Layer.size : (material.size.x > 0.f ? material.size.x : default_repeat));
|
||||
Layer.normalmap = (material.shader && material.shader->texture_conf.count("normalmap") > 0);
|
||||
Layer.specgloss = (material.shader && material.shader->texture_conf.count("specgloss") > 0);
|
||||
Layer.reflection = (std::isfinite(material.params[1].z) ? material.params[1].z : 0.f);
|
||||
}
|
||||
|
||||
int terrain_materials::layer_index(gfx::terrain_layer const &Layer)
|
||||
{
|
||||
for (auto const &existing : m_layers)
|
||||
if (existing.material == Layer.material && existing.size == Layer.size)
|
||||
return existing.index;
|
||||
|
||||
layer entry;
|
||||
entry.material = Layer.material;
|
||||
entry.size = Layer.size;
|
||||
entry.index = static_cast<int>(m_layers.size());
|
||||
read_material(entry);
|
||||
m_layers.push_back(entry);
|
||||
m_pending = true;
|
||||
|
||||
if (static_cast<int>(m_layers.size()) > m_capacity)
|
||||
{
|
||||
// more room: the arrays are made anew, and every layer copied again
|
||||
auto const capacity{m_capacity * 2};
|
||||
for (auto &array : m_arrays)
|
||||
gpu_storage(array, capacity, true);
|
||||
m_capacity = capacity;
|
||||
for (auto &existing : m_layers)
|
||||
{
|
||||
existing.copied = false;
|
||||
existing.attempts = 0;
|
||||
}
|
||||
gpu_changed();
|
||||
}
|
||||
return entry.index;
|
||||
}
|
||||
|
||||
std::size_t terrain_materials::acquire_slot(material_handle const Reuse)
|
||||
{
|
||||
if (Reuse != null_handle)
|
||||
{
|
||||
auto const lookup{m_slotof.find(Reuse)};
|
||||
if (lookup != m_slotof.end())
|
||||
{
|
||||
m_slots[lookup->second].used = true;
|
||||
return lookup->second;
|
||||
}
|
||||
}
|
||||
if (false == m_free.empty())
|
||||
{
|
||||
auto const index{m_free.back()};
|
||||
m_free.pop_back();
|
||||
m_slots[index].used = true;
|
||||
return index;
|
||||
}
|
||||
auto const index{m_slots.size()};
|
||||
auto const suffix{std::to_string(index)};
|
||||
slot entry;
|
||||
entry.weights[0] = make_texture("internal_src:terrain/weights0_" + suffix, texture_kind::weights);
|
||||
entry.weights[1] = make_texture("internal_src:terrain/weights1_" + suffix, texture_kind::weights);
|
||||
entry.table = make_texture("internal_src:terrain/table_" + suffix, texture_kind::table);
|
||||
entry.holes = make_texture("internal_src:terrain/holes_" + suffix, texture_kind::holes);
|
||||
// the textures get their content before the material is made of them, which checks they're there
|
||||
gpu_blank(entry);
|
||||
gpu_changed();
|
||||
auto const name = [&](texture_handle const Handle) { return "internal_src:" + m_textures.texture(Handle).name; };
|
||||
std::string const definition{
|
||||
"shader: terrain\n"
|
||||
"texture1: " + name(m_white) + "\n"
|
||||
"texture2: " + name(entry.weights[0]) + "\n"
|
||||
"texture3: " + name(entry.weights[1]) + "\n"
|
||||
"texture4: " + name(m_arrays[0].handle) + "\n"
|
||||
"texture5: " + name(m_arrays[1].handle) + "\n"
|
||||
"texture6: " + name(m_arrays[2].handle) + "\n"
|
||||
"texture7: " + name(entry.table) + "\n"
|
||||
"texture8: " + name(entry.holes) + "\n"};
|
||||
entry.material = m_materials.create_from_text("internal_src:terrain/chunk_" + suffix, definition);
|
||||
if (entry.material == null_handle || false == m_materials.material(entry.material).is_good)
|
||||
{
|
||||
ErrorLog("heightmap terrain: can't make the material of a chunk");
|
||||
m_state = -1;
|
||||
return static_cast<std::size_t>(-1);
|
||||
}
|
||||
entry.used = true;
|
||||
m_slots.push_back(entry);
|
||||
m_slotof.emplace(entry.material, index);
|
||||
return index;
|
||||
}
|
||||
|
||||
material_handle terrain_materials::make(material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement,
|
||||
int const HoleSamples, std::uint8_t const *Holes)
|
||||
{
|
||||
if (Layers.empty() || false == init())
|
||||
return null_handle;
|
||||
auto const count{std::min(Layers.size(), max_layers)};
|
||||
std::array<int, max_layers> indices{};
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
if (Layers[i].material == null_handle)
|
||||
return null_handle;
|
||||
indices[i] = layer_index(Layers[i]);
|
||||
}
|
||||
auto const slotindex{acquire_slot(Reuse)};
|
||||
if (slotindex == static_cast<std::size_t>(-1))
|
||||
return null_handle;
|
||||
auto &entry{m_slots[slotindex]};
|
||||
|
||||
// weights, four layers to a texture
|
||||
auto const painted{Weights != nullptr && Samples > 0 && count > 1};
|
||||
auto const side{painted ? Samples + 1 : 1};
|
||||
auto const samples{static_cast<std::size_t>(side) * side};
|
||||
std::vector<std::uint8_t> texels(samples * 4);
|
||||
for (std::size_t texture = 0; texture < 2; ++texture)
|
||||
{
|
||||
std::fill(texels.begin(), texels.end(), 0);
|
||||
for (std::size_t channel = 0; channel < 4; ++channel)
|
||||
{
|
||||
auto const layer{texture * 4 + channel};
|
||||
if (layer >= count)
|
||||
break;
|
||||
if (false == painted)
|
||||
{
|
||||
if (layer == 0)
|
||||
texels[channel] = 255;
|
||||
continue;
|
||||
}
|
||||
auto const *source{Weights + layer * samples};
|
||||
for (std::size_t i = 0; i < samples; ++i)
|
||||
texels[i * 4 + channel] = source[i];
|
||||
}
|
||||
gpu_weights(entry.weights[texture], side, texels.data());
|
||||
}
|
||||
entry.count = count;
|
||||
entry.layers = indices;
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
entry.rotations[i] = Layers[i].rotation;
|
||||
write_table(entry);
|
||||
|
||||
// the holes: the cells cut out entirely are left out of the mesh, the program of the holes cuts the rest of the openings
|
||||
auto const cut{Holes != nullptr && HoleSamples > 0};
|
||||
{
|
||||
auto const holeside{cut ? HoleSamples + 1 : 1};
|
||||
std::uint8_t const none{0};
|
||||
gpu_holes(entry.holes, holeside, cut ? Holes : &none);
|
||||
gpu_changed();
|
||||
}
|
||||
auto &material{m_materials.material(entry.material)};
|
||||
material.params[2] = glm::vec4(Placement.x, Placement.y, 1.f / std::max(1.f, Placement.z), static_cast<float>(side - 1));
|
||||
if (m_holesprogram == nullptr && cut)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_holesprogram = GfxRenderer->Fetch_Shader("terrain_holes");
|
||||
}
|
||||
catch (gfx::shader_exception const &e)
|
||||
{
|
||||
ErrorLog("heightmap terrain: can't use the shader of the holes: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
// the program of the holes discards fragments, which the depth prepass of the plain one never does
|
||||
material.shader = (cut && m_holesprogram != nullptr) ? m_holesprogram : m_program;
|
||||
return entry.material;
|
||||
}
|
||||
|
||||
void terrain_materials::write_table(slot const &Slot)
|
||||
{
|
||||
// what the layers are: index in the arrays, repeat, which maps they have, reflection
|
||||
std::array<float, max_layers * 2 * 4> table{};
|
||||
for (std::size_t i = 0; i < Slot.count; ++i)
|
||||
{
|
||||
auto const &layer{m_layers[Slot.layers[i]]};
|
||||
table[i * 4 + 0] = static_cast<float>(layer.index);
|
||||
table[i * 4 + 1] = 1.f / std::max(0.01f, layer.repeat);
|
||||
table[i * 4 + 2] = layer.normalmap ? 1.f : 0.f;
|
||||
table[i * 4 + 3] = layer.specgloss ? 1.f : 0.f;
|
||||
table[(max_layers + i) * 4 + 0] = layer.reflection;
|
||||
// the textures turned by the layer of the chunk (the same material can be turned differently in another layer)
|
||||
auto const angle{glm::radians(static_cast<double>(Slot.rotations[i]))};
|
||||
table[(max_layers + i) * 4 + 1] = static_cast<float>(std::cos(angle));
|
||||
table[(max_layers + i) * 4 + 2] = static_cast<float>(std::sin(angle));
|
||||
}
|
||||
gpu_table(Slot.table, table.data());
|
||||
gpu_changed();
|
||||
}
|
||||
|
||||
void terrain_materials::on_environment_change()
|
||||
{
|
||||
if (m_state <= 0)
|
||||
return;
|
||||
bool changed{false};
|
||||
for (auto &layer : m_layers)
|
||||
{
|
||||
auto const &material{m_materials.material(layer.material)};
|
||||
if (false == (material.update_on_season_change || material.update_on_weather_change))
|
||||
continue;
|
||||
// the reloaded material can have other textures, other maps and another size
|
||||
read_material(layer);
|
||||
layer.copied = false;
|
||||
layer.attempts = 0;
|
||||
changed = true;
|
||||
}
|
||||
if (false == changed)
|
||||
return;
|
||||
m_pending = true;
|
||||
// the tables of the chunks hold what the layers have, freed slots get theirs anew when they're used again
|
||||
for (auto const &entry : m_slots)
|
||||
if (entry.used)
|
||||
write_table(entry);
|
||||
}
|
||||
|
||||
void terrain_materials::release(material_handle const Material)
|
||||
{
|
||||
auto const lookup{m_slotof.find(Material)};
|
||||
if (lookup == m_slotof.end() || false == m_slots[lookup->second].used)
|
||||
return;
|
||||
m_slots[lookup->second].used = false;
|
||||
m_free.push_back(lookup->second);
|
||||
}
|
||||
|
||||
bool terrain_materials::copy_layer(layer &Layer)
|
||||
{
|
||||
auto const &material{m_materials.material(Layer.material)};
|
||||
auto const slot = [&](char const *Name, int const Fallback) {
|
||||
if (material.shader)
|
||||
{
|
||||
auto const lookup{material.shader->texture_conf.find(Name)};
|
||||
if (lookup != material.shader->texture_conf.end())
|
||||
return static_cast<int>(lookup->second.id);
|
||||
return -1;
|
||||
}
|
||||
return Fallback;
|
||||
};
|
||||
std::array<int, 3> const slots{slot("diffuse", 0), Layer.normalmap ? slot("normalmap", 1) : -1, Layer.specgloss ? slot("specgloss", 2) : -1};
|
||||
// all maps of the layer have to be ready, so they go together
|
||||
std::array<texture_source *, 3> sources{};
|
||||
for (std::size_t i = 0; i < slots.size(); ++i)
|
||||
{
|
||||
if (slots[i] < 0 || material.textures[slots[i]] == null_handle)
|
||||
continue;
|
||||
auto &texture{m_textures.mark_as_used(material.textures[slots[i]])};
|
||||
if (false == texture.is_ready)
|
||||
{
|
||||
// a texture which is on the card already may have dropped its data after the upload (create() would refuse it then),
|
||||
// one released by the sweep or never read has to be read first
|
||||
if (texture.data_state == resource_state::none)
|
||||
{
|
||||
texture.reload_on_use = false;
|
||||
texture.load();
|
||||
}
|
||||
if (texture.data_state == resource_state::failed)
|
||||
continue;
|
||||
if (false == texture.create())
|
||||
{
|
||||
if (++Layer.attempts < copy_attempts)
|
||||
return false;
|
||||
WriteLog("heightmap terrain: texture \"" + texture.name + "\" isn't ready, the terrain goes without it");
|
||||
continue; // waited long enough, the layer goes without it
|
||||
}
|
||||
}
|
||||
sources[i] = &texture;
|
||||
}
|
||||
// defaults for maps the layer doesn't have: grey, flat, without specular reflections
|
||||
std::array<std::array<float, 4>, 3> const defaults{{{0.5f, 0.5f, 0.5f, 1.f}, {0.5f, 0.5f, 1.f, 1.f}, {0.f, 1.f, 0.f, 1.f}}};
|
||||
for (std::size_t i = 0; i < m_arrays.size(); ++i)
|
||||
gpu_copy(m_arrays[i], Layer.index, sources[i], defaults[i]);
|
||||
Layer.copied = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void terrain_materials::update()
|
||||
{
|
||||
if (false == m_pending || m_state <= 0)
|
||||
return;
|
||||
|
||||
gpu_begin_copies();
|
||||
bool copied{false};
|
||||
m_pending = false;
|
||||
for (auto &layer : m_layers)
|
||||
{
|
||||
if (layer.copied)
|
||||
continue;
|
||||
if (copy_layer(layer))
|
||||
copied = true;
|
||||
else
|
||||
m_pending = true;
|
||||
}
|
||||
gpu_end_copies(copied);
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
142
rendering/common/terrainmaterials.h
Normal file
142
rendering/common/terrainmaterials.h
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
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 <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "rendering/renderer.h"
|
||||
#include "model/material.h"
|
||||
#include "model/Texture.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
// materials of the heightmap terrain (shader mat_terrain, mat_terrain_holes for chunks with holes), whichever api draws them.
|
||||
// the textures of the layer materials are copied into texture arrays shared by all chunks; each chunk gets a material
|
||||
// of its own, with textures holding the weights of its layers and a table of what its layers are.
|
||||
// what the layers and the chunks are is kept here; the textures are made and filled by the renderer (the gpu_ functions)
|
||||
class terrain_materials
|
||||
{
|
||||
public:
|
||||
terrain_materials(material_manager &Materials, texture_bank &Textures) : m_materials(Materials), m_textures(Textures) {}
|
||||
virtual ~terrain_materials() = default;
|
||||
|
||||
// see gfx_renderer::Terrain_Material. returns null_handle if the terrain shader can't be used
|
||||
material_handle make(material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement,
|
||||
int const HoleSamples = 0, std::uint8_t const *Holes = nullptr);
|
||||
void release(material_handle const Material);
|
||||
// copies the textures of new layers into the arrays; to be called before the frame is drawn
|
||||
void update();
|
||||
// the layer materials which change with the season or the weather were reloaded, their textures are copied again
|
||||
void on_environment_change();
|
||||
|
||||
protected:
|
||||
static constexpr std::size_t max_layers{8};
|
||||
|
||||
struct layer
|
||||
{
|
||||
material_handle material{null_handle};
|
||||
float size{0.f}; // as given by the chunk
|
||||
float repeat{8.f}; // metres the textures are repeated at
|
||||
int index{0}; // in the arrays
|
||||
bool normalmap{false};
|
||||
bool specgloss{false};
|
||||
float reflection{0.f};
|
||||
bool copied{false};
|
||||
int attempts{0};
|
||||
};
|
||||
struct texture_array
|
||||
{
|
||||
texture_handle handle{null_handle};
|
||||
int size{0};
|
||||
bool srgb{false};
|
||||
};
|
||||
struct slot
|
||||
{
|
||||
material_handle material{null_handle};
|
||||
std::array<texture_handle, 2> weights{null_handle, null_handle};
|
||||
texture_handle table{null_handle};
|
||||
texture_handle holes{null_handle}; // where the ground is cut, R8; a single texel of 0 without holes
|
||||
bool used{false};
|
||||
// what the table was made of, kept to make it again when a layer changes
|
||||
std::size_t count{0};
|
||||
std::array<int, max_layers> layers{};
|
||||
std::array<float, max_layers> rotations{};
|
||||
};
|
||||
// what a texture made here holds
|
||||
enum class texture_kind
|
||||
{
|
||||
white, // a single white texel, rgba8
|
||||
layers, // an array of the layer maps, rgba8 (srgb for the diffuse)
|
||||
weights, // weights of four layers, rgba8 with mipmaps, clamped
|
||||
table, // the table of the layers of a chunk, max_layers x 2 rgba32f, nearest
|
||||
holes // the mask of the holes, r8, led between the samples, clamped
|
||||
};
|
||||
|
||||
// the textures of the arrays: diffuse, normal maps, specular/gloss
|
||||
std::array<texture_array, 3> const &arrays() const
|
||||
{
|
||||
return m_arrays;
|
||||
}
|
||||
|
||||
// the objects the textures are drawn with (the copy of the layer maps); false: the terrain can't be drawn
|
||||
virtual bool gpu_init() = 0;
|
||||
// the object of a texture made by the bank, of the kind
|
||||
virtual void gpu_texture(texture_source &Texture, texture_kind const Kind) = 0;
|
||||
// the white texel
|
||||
virtual void gpu_white(texture_source &Texture) = 0;
|
||||
// room for Layers layers in the array; Fresh: the array had layers before, they go
|
||||
virtual void gpu_storage(texture_array const &Array, int const Layers, bool const Fresh) = 0;
|
||||
// the content of the textures of a new slot, before its material is made of them: no weights, a table of zeros, no holes
|
||||
virtual void gpu_blank(slot const &Slot) = 0;
|
||||
// Side x Side texels of rgba8, the mipmaps made of them
|
||||
virtual void gpu_weights(texture_handle const Texture, int const Side, std::uint8_t const *Texels) = 0;
|
||||
// max_layers x 2 texels of rgba32f
|
||||
virtual void gpu_table(texture_handle const Texture, float const *Table) = 0;
|
||||
// Side x Side samples of r8
|
||||
virtual void gpu_holes(texture_handle const Texture, int const Side, std::uint8_t const *Samples) = 0;
|
||||
// the copies of the layer maps into the arrays: begun, then a copy per array of each layer copied, ended; Copied: whether any
|
||||
// was made (the mipmaps of the arrays are made then)
|
||||
virtual void gpu_begin_copies() = 0;
|
||||
// Source: the map of the layer, or null when it has none, the array is filled with Default then
|
||||
virtual void gpu_copy(texture_array const &Array, int const Layer, texture_source *Source, std::array<float, 4> const &Default) = 0;
|
||||
virtual void gpu_end_copies(bool const Copied) = 0;
|
||||
// textures were made or given new content (the opengl renderer forgets the textures bound)
|
||||
virtual void gpu_changed() {}
|
||||
|
||||
material_manager &m_materials;
|
||||
texture_bank &m_textures;
|
||||
|
||||
private:
|
||||
bool init();
|
||||
texture_handle make_texture(std::string const &Name, texture_kind const Kind);
|
||||
void read_material(layer &Layer);
|
||||
int layer_index(gfx::terrain_layer const &Layer);
|
||||
void write_table(slot const &Slot);
|
||||
bool copy_layer(layer &Layer);
|
||||
std::size_t acquire_slot(material_handle const Reuse);
|
||||
|
||||
int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used
|
||||
texture_handle m_white{null_handle};
|
||||
std::array<texture_array, 3> m_arrays; // diffuse, normal maps, specular/gloss
|
||||
int m_capacity{0}; // layers the arrays have room for
|
||||
std::vector<layer> m_layers;
|
||||
bool m_pending{false};
|
||||
std::vector<slot> m_slots;
|
||||
std::vector<std::size_t> m_free;
|
||||
std::unordered_map<material_handle, std::size_t> m_slotof;
|
||||
std::shared_ptr<gfx::shader_program> m_program; // of the chunks, mat_terrain
|
||||
std::shared_ptr<gfx::shader_program> m_holesprogram; // of the chunks with holes, mat_terrain_holes
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
343
rendering/common/weatherstate.cpp
Normal file
343
rendering/common/weatherstate.cpp
Normal file
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/weatherstate.h"
|
||||
|
||||
#include "environment/starcatalog.h"
|
||||
#include "environment/weather.h"
|
||||
#include "simulation/simulationenvironment.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Timer.h"
|
||||
#include "widgets/weatherpanel.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// how many times the cell of a genus fits in the tile of the shape noise, and its spread in the tile of the coverage noise
|
||||
double const SHAPE_TILE_CELLS{4.0};
|
||||
double const COVERAGE_TILE_SPREADS{4.0};
|
||||
// size of the patches of the ground fog
|
||||
float const FOG_PATCH_SIZE{300.f};
|
||||
// the haze the atmosphere draws in the sky already, as an extinction: about the haze of a day with 30 km of visibility
|
||||
float const SKY_HAZE_EXTINCTION{3.f / 30000.f};
|
||||
// near end of the fog volume (view depth, metres)
|
||||
float const FOG_VOLUME_NEAR{0.5f};
|
||||
// bands the panorama is drawn in, one a frame
|
||||
int const PANORAMA_BANDS{8};
|
||||
// brightness of a star of magnitude 0 in the units of the scene
|
||||
float const STAR_BRIGHTNESS{4.f};
|
||||
// stars dimmer than this aren't drawn
|
||||
float const STAR_LIMIT{6.5f};
|
||||
|
||||
float luminance(glm::vec3 const &Color)
|
||||
{
|
||||
return glm::dot(Color, glm::vec3{0.2126f, 0.7152f, 0.0722f});
|
||||
}
|
||||
|
||||
// the position in a tiling noise, of a period in metres, kept small for the single precision of the shaders
|
||||
double wrap(double const Value, double const Period)
|
||||
{
|
||||
return Value - Period * std::floor(Value / Period);
|
||||
}
|
||||
|
||||
// the colour of a star of a colour temperature, linear, of luminance 1, somewhat paler than the black body: the eye sees most
|
||||
// stars white
|
||||
glm::vec3 star_color(float const Temperature)
|
||||
{
|
||||
auto const t{std::clamp(Temperature, 1500.f, 40000.f) / 100.f};
|
||||
glm::vec3 color;
|
||||
color.r = (t <= 66.f) ? 255.f : 329.698727446f * std::pow(t - 60.f, -0.1332047592f);
|
||||
color.g = (t <= 66.f) ? 99.4708025861f * std::log(t) - 161.1195681661f : 288.1221695283f * std::pow(t - 60.f, -0.0755148492f);
|
||||
color.b = (t >= 66.f) ? 255.f : (t <= 19.f) ? 0.f : 138.5177312231f * std::log(t - 10.f) - 305.0447927307f;
|
||||
color = glm::pow(glm::clamp(color / 255.f, glm::vec3{0.f}, glm::vec3{1.f}), glm::vec3{2.2f});
|
||||
color /= std::max(luminance(color), 1e-3f);
|
||||
return glm::mix(color, glm::vec3{1.f}, 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
weather_state::quality_level weather_state::quality(int const Clouds, int const Fog)
|
||||
{
|
||||
quality_level level;
|
||||
switch (std::clamp(Clouds, 1, 4))
|
||||
{
|
||||
case 1: level = {4, 32, 3, false, {512, 128}, {}}; break;
|
||||
case 2: level = {2, 48, 4, true, {1024, 256}, {}}; break;
|
||||
case 3: level = {2, 80, 6, true, {1024, 256}, {}}; break;
|
||||
default: level = {2, 128, 8, true, {2048, 512}, {}}; break;
|
||||
}
|
||||
switch (std::clamp(Fog, 1, 4))
|
||||
{
|
||||
case 1: level.fog_grid = {80, 45, 48}; break;
|
||||
case 2: level.fog_grid = {160, 90, 64}; break;
|
||||
case 3: level.fog_grid = {160, 90, 96}; break;
|
||||
default: level.fog_grid = {240, 135, 128}; break;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
weather_state::weather_state()
|
||||
{
|
||||
// a grey fog and no clouds until the first update
|
||||
for (auto &color : m_weather.fog_horizon)
|
||||
color = glm::vec4{0.5f, 0.5f, 0.5f, 1.f};
|
||||
m_weather.fog_haze = glm::vec4{3.f / 10000.f, 1.f / 800.f, SKY_HAZE_EXTINCTION, 1.f};
|
||||
m_weather.fog_light = glm::vec4{0.f, 1.f, 0.6f, 1.f / 5000.f};
|
||||
}
|
||||
|
||||
void weather_state::update(float const Atmosphereweight, bool const Skyview, glm::vec3 const &Keylight, glm::vec3 const &Keycolor, glm::vec3 const &Ambient, glm::vec3 const &Fogcolor, glm::dvec3 const &Camera)
|
||||
{
|
||||
auto const &weather{simulation::Environment.weather()};
|
||||
auto const &conditions{weather.conditions()};
|
||||
m_fog_volume_valid = false;
|
||||
m_clouds_view_valid = false;
|
||||
++m_frame;
|
||||
|
||||
auto const height{static_cast<float>(Camera.y) - weather.ground()};
|
||||
auto const time{weather.time()};
|
||||
auto const sun{glm::normalize(Keylight)};
|
||||
|
||||
// the fog. the visibility is where a dark object fades to 5%: the extinction is 3 over it
|
||||
auto const haze{3.f / conditions.visibility};
|
||||
auto const hazerate{1.f / conditions.haze_height};
|
||||
m_weather.fog_haze = glm::vec4{haze, hazerate, SKY_HAZE_EXTINCTION, 1.f};
|
||||
if (conditions.groundfog_thickness > 0.f)
|
||||
{
|
||||
auto const thickness{conditions.groundfog_thickness};
|
||||
m_weather.fog_ground = glm::vec4{std::max(3.f / conditions.groundfog_visibility - haze, 0.f), 1.f / (0.25f * thickness), 0.75f * thickness, conditions.fog_noise};
|
||||
}
|
||||
else
|
||||
m_weather.fog_ground = glm::vec4{0.f, 1.f, 0.f, conditions.fog_noise};
|
||||
// the patches drift with the wind, slower than the clouds
|
||||
auto const drift{glm::dvec2{weather.cloud_offset()} * 0.3};
|
||||
auto const patch{static_cast<double>(FOG_PATCH_SIZE)};
|
||||
m_weather.fog_noise = glm::vec4{static_cast<float>(wrap(Camera.x - drift.x, patch)), static_cast<float>(wrap(Camera.y, patch)), static_cast<float>(wrap(Camera.z - drift.y, patch)), 1.f / FOG_PATCH_SIZE};
|
||||
// the extinction at the height of the camera tells how far things are hidden
|
||||
auto const extinction{haze * std::exp(-hazerate * std::max(height, 0.f)) +
|
||||
m_weather.fog_ground.x * std::exp(-m_weather.fog_ground.y * std::max(height - m_weather.fog_ground.z, 0.f))};
|
||||
// 2% left at 1.3 visibilities; the draw range is twice this
|
||||
m_fog_range = std::clamp(3.f / std::max(extinction, 1e-7f) / 1.5f, 5.f, 1e6f);
|
||||
|
||||
// the share of the light of the fog the sun gives, which the shadows hold back
|
||||
auto const sunlight{luminance(Keycolor) * 1.5f * std::max(sun.y, 0.f)};
|
||||
auto const skylight{luminance(Ambient) * 0.65f};
|
||||
m_sun_share = (sunlight + skylight > 1e-6f) ? sunlight / (sunlight + skylight) : 0.f;
|
||||
|
||||
// clouds
|
||||
auto const layers{std::min(conditions.clouds.size(), gl::MAX_CLOUD_LAYERS)};
|
||||
m_clouds_drawn = (Global.gfx_clouds > 0) && (layers > 0);
|
||||
m_average_transmittance = 1.f;
|
||||
auto const velocity{weather.cloud_velocity()};
|
||||
auto const speed{glm::length(velocity)};
|
||||
auto const wind{speed > 1e-3f ? glm::dvec2{velocity / speed} : glm::dvec2{1.0, 0.0}};
|
||||
m_weather.cloud_wind = glm::vec4{static_cast<float>(wind.x), static_cast<float>(wind.y), speed, m_cloud_detail_scale};
|
||||
for (std::size_t i = 0; i < gl::MAX_CLOUD_LAYERS; ++i)
|
||||
{
|
||||
auto &target{m_weather.cloud_layers[i]};
|
||||
if (i >= layers)
|
||||
{
|
||||
target = gl::cloud_layer_ubs{};
|
||||
continue;
|
||||
}
|
||||
auto const &layer{conditions.clouds[i]};
|
||||
auto const &genus{cloud_genus_info(layer.type)};
|
||||
target.heights = glm::vec4{layer.base, layer.top, 1.f / std::max(layer.top - layer.base, 1.f), static_cast<float>(layer.type)};
|
||||
auto const shapetile{static_cast<double>(genus.cell) * SHAPE_TILE_CELLS};
|
||||
auto const coveragetile{static_cast<double>(genus.spread) * COVERAGE_TILE_SPREADS};
|
||||
target.shape = glm::vec4{layer.coverage, genus.extinction * layer.density, static_cast<float>(1.0 / shapetile), genus.erosion};
|
||||
target.profile = glm::vec4{genus.bottom, genus.top, genus.top_variation, genus.anvil};
|
||||
// the higher, the faster the wind carries them
|
||||
auto const offset{glm::dvec2{weather.cloud_offset()} * (1.0 + static_cast<double>(layer.base) / 8000.0)};
|
||||
// the camera's place in the noise, in the space stretched along the wind as the shaders take it
|
||||
auto const place{glm::dvec2{Camera.x, Camera.z} - offset};
|
||||
auto const along{glm::dot(place, wind)};
|
||||
auto const stretched{(place - wind * along) + wind * (along / std::max(static_cast<double>(genus.stretch), 1.0))};
|
||||
target.motion = glm::vec4{static_cast<float>(wrap(stretched.x, shapetile)), static_cast<float>(wrap(stretched.y, shapetile)), static_cast<float>(1.0 / coveragetile), genus.stretch};
|
||||
// the clouds change their shapes slowly; the seed moves the layers apart in the noise
|
||||
auto const evolution{std::fmod(static_cast<double>(conditions.seed) * 0.6180339887 + static_cast<double>(i) * 0.37 + time * 0.00002, 1.0)};
|
||||
target.extra = glm::vec4{genus.flat ? 1.f : 0.f, static_cast<float>(evolution), static_cast<float>(wrap(stretched.x, coveragetile)), static_cast<float>(wrap(stretched.y, coveragetile))};
|
||||
m_average_transmittance *= 1.f - std::clamp(layer.coverage * genus.opacity * std::min(layer.density, 1.5f), 0.f, 1.f);
|
||||
}
|
||||
if (Global.gfx_clouds <= 0)
|
||||
m_average_transmittance = 1.f;
|
||||
m_weather.fog_light = glm::vec4{m_sun_share, m_average_transmittance, 0.6f, fog_density()};
|
||||
m_weather.weather_camera = glm::vec4{height, static_cast<float>(std::fmod(time, 100000.0)), m_clouds_drawn ? 1.f : 0.f, m_clouds_drawn ? static_cast<float>(layers) : 0.f};
|
||||
m_weather.weather_sun_direction = glm::vec4{sun, sun.y > -0.02f ? 1.f : 0.f};
|
||||
m_weather.weather_sun_color = glm::vec4{Keycolor, m_cloud_sun_scale};
|
||||
m_weather.weather_ambient = glm::vec4{Ambient * 1.3f, m_cloud_sky_scale};
|
||||
m_weather.fog_volume_params = glm::vec4{0.f, FOG_VOLUME_NEAR, 1.f, 1.f};
|
||||
m_weather.fog_volume_target = glm::vec4{1.f, 1.f, Global.gfx_fog_range, 1.f - Atmosphereweight};
|
||||
|
||||
// the colours of the horizon: from the atmosphere on the gpu, the old fog colour otherwise
|
||||
m_horizon_on_gpu = (Atmosphereweight > 0.f) && Skyview;
|
||||
if (false == m_horizon_on_gpu)
|
||||
{
|
||||
for (auto &color : m_weather.fog_horizon)
|
||||
color = glm::vec4{Fogcolor, 1.f};
|
||||
}
|
||||
|
||||
// the shadows of the clouds
|
||||
m_shadows_wanted = m_clouds_drawn && Global.gfx_clouds_shadows && (sun.y > 0.02f);
|
||||
m_weather.cloud_shadow_params = glm::vec4{0.f};
|
||||
m_report = to_metar(conditions);
|
||||
}
|
||||
|
||||
void weather_state::place_shadows(int const Size, glm::dvec3 const &Camera)
|
||||
{
|
||||
// a map snapped to its texels so it doesn't crawl as the camera moves
|
||||
auto const extent{static_cast<double>(Global.gfx_clouds_shadows_range)};
|
||||
auto const texel{extent / static_cast<double>(Size)};
|
||||
auto const centre{glm::dvec2{std::round(Camera.x / texel) * texel - Camera.x, std::round(Camera.z / texel) * texel - Camera.z}};
|
||||
m_weather.cloud_shadow_params = glm::vec4{static_cast<float>(centre.x), static_cast<float>(centre.y), static_cast<float>(1.0 / extent), 0.f};
|
||||
}
|
||||
|
||||
void weather_state::shadows_drawn(bool const Drawn)
|
||||
{
|
||||
m_shadows_valid = Drawn;
|
||||
if (Drawn)
|
||||
m_weather.cloud_shadow_params.w = 1.f;
|
||||
}
|
||||
|
||||
void weather_state::panorama_resized()
|
||||
{
|
||||
m_panorama_full = false;
|
||||
m_panorama_band = 0;
|
||||
}
|
||||
|
||||
void weather_state::panorama_band(glm::ivec2 const &Size, int &First, int &Rows)
|
||||
{
|
||||
auto const bandrows{(Size.y + PANORAMA_BANDS - 1) / PANORAMA_BANDS};
|
||||
First = m_panorama_full ? m_panorama_band * bandrows : 0;
|
||||
Rows = m_panorama_full ? bandrows : Size.y;
|
||||
m_panorama_band = m_panorama_full ? (m_panorama_band + 1) % PANORAMA_BANDS : 0;
|
||||
m_panorama_full = true;
|
||||
}
|
||||
|
||||
bool weather_state::continuity(view_state const &Previous, glm::dvec3 const &Camera)
|
||||
{
|
||||
// a jump of the camera (another vehicle, the free camera) leaves nothing to blend with
|
||||
return Previous.valid && glm::length(Previous.camera - Camera) < 50.0;
|
||||
}
|
||||
|
||||
glm::vec3 weather_state::fog_jitter(bool const Temporal) const
|
||||
{
|
||||
// the cells are sampled at another point in every frame: along the depth in the Halton sequence of 2, across the view in
|
||||
// those of 3 and 5 (fog_inject.comp moves the point of every cell by a value of its own)
|
||||
auto const halton = [](int Index, int const Base) {
|
||||
float value{0.f}, fraction{1.f};
|
||||
for (; Index > 0; Index /= Base)
|
||||
{
|
||||
fraction /= static_cast<float>(Base);
|
||||
value += fraction * static_cast<float>(Index % Base);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
auto const sample{(m_frame % 64) + 1};
|
||||
return Temporal ? glm::vec3{halton(sample, 3), halton(sample, 5), halton(sample, 2)} : glm::vec3{0.5f};
|
||||
}
|
||||
|
||||
glm::vec4 weather_state::fog_slicing(int const Slices, glm::vec3 const &Jitter)
|
||||
{
|
||||
auto const farthest{std::max(Global.gfx_fog_range, FOG_VOLUME_NEAR * 4.f)};
|
||||
return glm::vec4{FOG_VOLUME_NEAR, farthest, static_cast<float>(Slices) / std::log((farthest + gl::FOG_VOLUME_BIAS) / (FOG_VOLUME_NEAR + gl::FOG_VOLUME_BIAS)), Jitter.z};
|
||||
}
|
||||
|
||||
void weather_state::fog_made(glm::vec4 const &Slicing, glm::ivec3 const &Grid)
|
||||
{
|
||||
m_fog_slicing = Slicing;
|
||||
m_fog_grid = Grid;
|
||||
m_fog_volume_valid = true;
|
||||
}
|
||||
|
||||
void weather_state::setup_pass(bool const Volume, glm::ivec2 const &Size)
|
||||
{
|
||||
auto const volume{Volume && m_fog_volume_valid && (Global.gfx_fog == 1)};
|
||||
m_weather.fog_volume_params = volume ? glm::vec4{1.f, m_fog_slicing.x, m_fog_slicing.z, static_cast<float>(m_fog_grid.z)} : glm::vec4{0.f, FOG_VOLUME_NEAR, 1.f, 1.f};
|
||||
m_weather.fog_volume_target.x = 1.f / static_cast<float>(std::max(Size.x, 1));
|
||||
m_weather.fog_volume_target.y = 1.f / static_cast<float>(std::max(Size.y, 1));
|
||||
m_weather.fog_volume_target.z = volume ? m_fog_slicing.y : Global.gfx_fog_range;
|
||||
}
|
||||
|
||||
std::vector<glm::vec4> weather_state::star_vertices()
|
||||
{
|
||||
// the stars, in the equatorial frame
|
||||
std::vector<glm::vec4> stars;
|
||||
auto const *catalogue{star_catalogue::stars()};
|
||||
for (std::size_t i = 0; i < star_catalogue::size(); ++i)
|
||||
{
|
||||
auto const &star{catalogue[i]};
|
||||
auto const magnitude{static_cast<float>(star.magnitude) * 0.01f};
|
||||
if (magnitude > STAR_LIMIT)
|
||||
continue;
|
||||
auto const ascension{glm::radians(static_cast<float>(star.right_ascension) * 0.01f)};
|
||||
auto const declination{glm::radians(static_cast<float>(star.declination) * 0.01f)};
|
||||
stars.emplace_back(std::cos(declination) * std::cos(ascension), std::cos(declination) * std::sin(ascension), std::sin(declination), magnitude);
|
||||
// a phase of the twinkling which doesn't repeat from star to star
|
||||
auto const phase{std::fmod(static_cast<float>(i) * 0.6180339887f, 1.f)};
|
||||
stars.emplace_back(star_color(static_cast<float>(star.temperature) * 10.f), phase);
|
||||
}
|
||||
return stars;
|
||||
}
|
||||
|
||||
bool weather_state::stars(float const Sunangle, double const Siderealtime, double const Latitude, glm::mat4 const &View, glm::mat4 &Toworld, glm::mat4 &Toview, glm::vec4 &Params) const
|
||||
{
|
||||
if (!Global.gfx_stars)
|
||||
return false;
|
||||
// the stars come out between the civil and the nautical twilight
|
||||
auto const night{1.f - glm::smoothstep(-12.f, -3.f, Sunangle)};
|
||||
if (night <= 0.f)
|
||||
return false;
|
||||
// the equatorial frame (z to the pole, x to the vernal equinox) to the sky of the observer (x west, y up, z north)
|
||||
auto const theta{glm::radians(Siderealtime)};
|
||||
auto const phi{glm::radians(Latitude)};
|
||||
auto const st{static_cast<float>(std::sin(theta))}, ct{static_cast<float>(std::cos(theta))};
|
||||
auto const sp{static_cast<float>(std::sin(phi))}, cp{static_cast<float>(std::cos(phi))};
|
||||
glm::mat3 equatorial;
|
||||
equatorial[0] = glm::vec3{st, cp * ct, -sp * ct};
|
||||
equatorial[1] = glm::vec3{-ct, cp * st, -sp * st};
|
||||
equatorial[2] = glm::vec3{0.f, sp, cp};
|
||||
Toworld = glm::mat4{equatorial};
|
||||
Toview = glm::mat4{glm::mat3{View}};
|
||||
Params = glm::vec4{STAR_BRIGHTNESS * Global.gfx_stars_brightness * night, static_cast<float>(std::fmod(Timer::GetTime(), 10000.0)), 2.5f, 0.f};
|
||||
return true;
|
||||
}
|
||||
|
||||
void weather_state::draw_debug_ui()
|
||||
{
|
||||
char const *fogmodes[]{"analytic (along the ray, as the old fog)", "volumetric"};
|
||||
ImGui::Combo("fog (gfx.fog)", &Global.gfx_fog, fogmodes, IM_ARRAYSIZE(fogmodes));
|
||||
char const *levels[]{"low", "medium", "high", "ultra"};
|
||||
auto fogquality{std::clamp(Global.gfx_fog_quality, 1, 4) - 1};
|
||||
if (ImGui::Combo("fog volume quality (gfx.fog.quality)", &fogquality, levels, IM_ARRAYSIZE(levels)))
|
||||
Global.gfx_fog_quality = fogquality + 1;
|
||||
ImGui::SliderFloat("fog volume depth, m (gfx.fog.range)", &Global.gfx_fog_range, 100.f, 10000.f, "%.0f", ImGuiSliderFlags_Logarithmic);
|
||||
ImGui::SliderFloat("lamps in the fog (gfx.fog.lights)", &Global.gfx_fog_lights, 0.f, 4.f);
|
||||
ImGui::Checkbox("shadows of the sun and headlights in the fog (gfx.fog.shadows)", &Global.gfx_fog_shadows);
|
||||
ImGui::Checkbox("fog blended over frames (gfx.fog.temporal)", &Global.gfx_fog_temporal);
|
||||
ImGui::SliderFloat("share of a new frame in the fog (gfx.fog.temporal.share)", &Global.gfx_fog_temporal_share, 0.02f, 1.f);
|
||||
char const *cloudlevels[]{"none", "low", "medium", "high", "ultra"};
|
||||
ImGui::Combo("clouds (gfx.clouds)", &Global.gfx_clouds, cloudlevels, IM_ARRAYSIZE(cloudlevels));
|
||||
ImGui::SliderFloat("cloud distance, m (gfx.clouds.distance)", &Global.gfx_clouds_distance, 5000.f, 200000.f, "%.0f", ImGuiSliderFlags_Logarithmic);
|
||||
ImGui::Checkbox("cloud shadows (gfx.clouds.shadows)", &Global.gfx_clouds_shadows);
|
||||
ImGui::SliderFloat("cloud shadow map, m (gfx.clouds.shadows.range)", &Global.gfx_clouds_shadows_range, 1000.f, 100000.f, "%.0f", ImGuiSliderFlags_Logarithmic);
|
||||
ImGui::SliderFloat("cloud sunlight (tuning, not saved)", &m_cloud_sun_scale, 0.25f, 3.f);
|
||||
ImGui::SliderFloat("cloud sky light (tuning, not saved)", &m_cloud_sky_scale, 0.f, 3.f);
|
||||
ImGui::SliderFloat("cloud edge detail (tuning, not saved)", &m_cloud_detail_scale, 0.f, 3.f);
|
||||
ImGui::Checkbox("stars (gfx.stars)", &Global.gfx_stars);
|
||||
ImGui::SliderFloat("star brightness", &Global.gfx_stars_brightness, 0.f, 8.f);
|
||||
ImGui::Text("fog range %.0f m, sun share of the fog light %.2f, clouds let through %.2f on average", m_fog_range, m_sun_share, m_average_transmittance);
|
||||
ImGui::Separator();
|
||||
ui::weather_controls(simulation::Environment.weather());
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
168
rendering/common/weatherstate.h
Normal file
168
rendering/common/weatherstate.h
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
// the weather the renderers draw (the part of the stage F of claude/gl46-renderer-features-plan.md which isn't the work of the
|
||||
// gpu, taken out of opengl46_weather for the vulkan renderer, stage P2 of claude/vulkan14-renderer-plan.md): the block of the
|
||||
// weather of the shaders (gl::weather_ubs) made from the weather of the simulation (environment/weather.h) for the frame and for
|
||||
// each pass, the quality levels, the place of the map of the shadows of the clouds, the bands of the panorama of the clouds, the
|
||||
// sampling of the fog volume over the frames, and the stars of the catalogue (environment/starcatalog.h)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gl/ubo.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
class weather_state
|
||||
{
|
||||
public:
|
||||
struct quality_level
|
||||
{
|
||||
int divisor; // of the resolution of the clouds of the main view
|
||||
int steps; // samples through a layer at most
|
||||
int light_steps; // samples towards the sun
|
||||
bool detail; // with the small noise
|
||||
glm::ivec2 panorama; // size of the panorama
|
||||
glm::ivec3 fog_grid; // cells of the fog volume
|
||||
};
|
||||
static quality_level quality(int const Clouds, int const Fog);
|
||||
|
||||
// the camera of the previous frame of the main view, for the blending over frames
|
||||
struct view_state
|
||||
{
|
||||
glm::mat4 projection_view{1.f}; // world axes to clip space, camera at the origin
|
||||
glm::dvec3 camera{0.0};
|
||||
glm::vec4 slicing{0.f};
|
||||
bool valid{false};
|
||||
};
|
||||
|
||||
// a grey fog and no clouds until the first update
|
||||
weather_state();
|
||||
|
||||
// what the weather of the simulation is for the frame: the block, whether the clouds are drawn and cast shadows. before the
|
||||
// gpu work of the frame. Atmosphereweight: share of the atmosphere in the sky, Skyview: its sky-view table is there to
|
||||
// give the colours of the horizon; Keylight: world direction towards the sun or the moon, Keycolor: its colour as the
|
||||
// shading gets it, Ambient: the ambient light of the sky, Fogcolor: the old colour of the fog
|
||||
void update(float const Atmosphereweight, bool const Skyview, glm::vec3 const &Keylight, glm::vec3 const &Keycolor, glm::vec3 const &Ambient, glm::vec3 const &Fogcolor, glm::dvec3 const &Camera);
|
||||
// the shadows of the clouds are to be drawn in this frame (after update())
|
||||
bool shadows_wanted() const
|
||||
{
|
||||
return m_shadows_wanted;
|
||||
}
|
||||
// the place of the map of the shadows of Size texels, snapped to its texels so it doesn't crawl as the camera moves
|
||||
void place_shadows(int const Size, glm::dvec3 const &Camera);
|
||||
// the map of the shadows was drawn (or not) in this frame
|
||||
void shadows_drawn(bool const Drawn);
|
||||
// the rows of the panorama of the clouds of Size to draw in this frame: a band of it, all of it the first time after
|
||||
// panorama_resized()
|
||||
void panorama_band(glm::ivec2 const &Size, int &First, int &Rows);
|
||||
void panorama_resized();
|
||||
// a camera close enough to the one of the previous frame to blend with
|
||||
static bool continuity(view_state const &Previous, glm::dvec3 const &Camera);
|
||||
// the point the cells of the fog volume are sampled at in this frame (Temporal: it moves from frame to frame) and the slicing of
|
||||
// the volume of Slices slices it makes
|
||||
glm::vec3 fog_jitter(bool const Temporal) const;
|
||||
static glm::vec4 fog_slicing(int const Slices, glm::vec3 const &Jitter);
|
||||
// the clouds of the main view and the fog volume were made in this frame
|
||||
void clouds_made()
|
||||
{
|
||||
m_clouds_view_valid = true;
|
||||
}
|
||||
void fog_made(glm::vec4 const &Slicing, glm::ivec3 const &Grid);
|
||||
// the fog of a pass: the volume of the main view (made in the same frame), or along the ray. Size: the target of the pass
|
||||
void setup_pass(bool const Volume, glm::ivec2 const &Size);
|
||||
|
||||
gl::weather_ubs const &block() const
|
||||
{
|
||||
return m_weather;
|
||||
}
|
||||
// the colours of the horizon come from the sky-view table of the atmosphere on the gpu, not from the cpu
|
||||
bool horizon_on_gpu() const
|
||||
{
|
||||
return m_horizon_on_gpu;
|
||||
}
|
||||
// true when the volumetric clouds are drawn (the sky leaves its own overcast look then)
|
||||
bool clouds_drawn() const
|
||||
{
|
||||
return m_clouds_drawn;
|
||||
}
|
||||
bool clouds_view_valid() const
|
||||
{
|
||||
return m_clouds_view_valid;
|
||||
}
|
||||
// true when the clouds cast shadows (the sunlight isn't dimmed by the overcast then, the shadows do it)
|
||||
bool cloud_shadows() const
|
||||
{
|
||||
return m_shadows_valid;
|
||||
}
|
||||
bool fog_volume_valid() const
|
||||
{
|
||||
return m_fog_volume_valid;
|
||||
}
|
||||
// fog density of a regular draw (model_ubs.fog_density): 1 / the fog range
|
||||
float fog_density() const
|
||||
{
|
||||
return 1.f / m_fog_range;
|
||||
}
|
||||
// distance the fog hides things at, for the draw range: a dark object fades to about 2% there
|
||||
float fog_range() const
|
||||
{
|
||||
return m_fog_range;
|
||||
}
|
||||
// frames updated, the sequence of the sampling over frames
|
||||
int frame() const
|
||||
{
|
||||
return m_frame;
|
||||
}
|
||||
|
||||
// the stars, in the equatorial frame: for each star the direction and the magnitude, then the colour and the phase of its
|
||||
// twinkling
|
||||
static std::vector<glm::vec4> star_vertices();
|
||||
// whether the stars are drawn in the sky pass, and what with: the rotation from the equatorial frame to the world, from the
|
||||
// world to the view of the pass (View: rotation of its camera), the brightness, the time, the size of the points
|
||||
bool stars(float const Sunangle, double const Siderealtime, double const Latitude, glm::mat4 const &View, glm::mat4 &Toworld, glm::mat4 &Toview, glm::vec4 &Params) const;
|
||||
|
||||
// controls of the debug window, Volumeavailable: the renderer has the fog volume
|
||||
void draw_debug_ui();
|
||||
|
||||
// the cameras of the clouds and of the fog volume of the previous frame
|
||||
view_state previous_clouds;
|
||||
view_state previous_fog;
|
||||
|
||||
private:
|
||||
gl::weather_ubs m_weather{};
|
||||
bool m_horizon_on_gpu{false};
|
||||
bool m_shadows_wanted{false};
|
||||
bool m_shadows_valid{false};
|
||||
bool m_clouds_drawn{false};
|
||||
bool m_clouds_view_valid{false};
|
||||
int m_panorama_band{0};
|
||||
bool m_panorama_full{false};
|
||||
glm::ivec3 m_fog_grid{0};
|
||||
bool m_fog_volume_valid{false};
|
||||
float m_fog_range{5000.f};
|
||||
glm::vec4 m_fog_slicing{0.f};
|
||||
int m_frame{0};
|
||||
|
||||
// tuning of the look of the clouds in the debug window, not saved: scales of their sunlight, of their sky light and of the
|
||||
// erosion of their edges by the detail noise (weather_sun_color.w, weather_ambient.w, cloud_wind.w)
|
||||
float m_cloud_sun_scale{1.f};
|
||||
float m_cloud_sky_scale{1.f};
|
||||
float m_cloud_detail_scale{1.f};
|
||||
// what the debug window shows
|
||||
float m_sun_share{0.f};
|
||||
float m_average_transmittance{1.f};
|
||||
std::string m_report;
|
||||
};
|
||||
|
||||
} // namespace gfx
|
||||
Reference in New Issue
Block a user