mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 17:24:53 +02:00
- In the top view of the tracks work area, the renderer's reach is the half diagonal of the screen. It used to be three extents, which cut wide windows short. - Cells far from the camera no longer draw only their tracks: the terrain shapes are drawn too. Models keep their own ranges. - In that view, the heightmap streamer loads chunks as far as the corners, up to 50 km. The detail comes from the size of a pixel, not from the distance. - The walk over the chunks around the camera is skipped while it would ask for nothing, so a wide reach costs little. - A setting, kept in the editor settings, leaves out the ballast of the railway in the top view, so the ground under the tracks shows. It is in the View menu and next to the extent slider. - Self-test: ortho <extent> [noballast|ballast], orthophoto off.
1938 lines
66 KiB
C++
1938 lines
66 KiB
C++
/*
|
|
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 "editor/editorTerrainStreamer.hpp"
|
|
|
|
#include "model/vertex.h"
|
|
#include "rendering/renderer.h"
|
|
#include "scene/scene.h"
|
|
#include "simulation/simulation.h"
|
|
#include "utilities/Globals.h"
|
|
#include "utilities/Logs.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
|
|
terrain_streamer EditorTerrain;
|
|
|
|
namespace
|
|
{
|
|
|
|
int constexpr worker_count{2};
|
|
std::size_t constexpr max_loading{12}; // chunks asked from the workers at a time
|
|
std::size_t constexpr max_meshing{12}; // meshes asked from the workers at a time
|
|
double constexpr upload_budget{0.004}; // seconds of a frame for taking over finished work
|
|
int constexpr materials_per_frame{6};
|
|
double constexpr look_ahead{8.0}; // seconds; chunks are loaded around where the camera will be by then
|
|
double constexpr look_ahead_limit{2000.0}; // metres
|
|
double constexpr pixel_distance{1300.0}; // m; a pixel of the top view is as big as one of the 3D view this far away
|
|
int constexpr top_radius{200}; // chunks the top view of the tracks loads around its centre at most
|
|
double constexpr settled_walk{1.0}; // seconds; the chunks around the camera are looked over this often when nothing is coming
|
|
double constexpr region_half{scene::EU07_REGIONSIDESECTIONCOUNT * scene::EU07_SECTIONSIZE * 0.5 - heightmap::chunk_size};
|
|
double constexpr kPi{3.14159265358979323846};
|
|
|
|
double steady_seconds()
|
|
{
|
|
return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
std::uint64_t geometry_key(gfx::geometry_handle const &Geometry)
|
|
{
|
|
return (static_cast<std::uint64_t>(Geometry.bank) << 32) | Geometry.chunk;
|
|
}
|
|
|
|
bool in_region(glm::dvec2 const &Point)
|
|
{
|
|
return std::abs(Point.x) < region_half && std::abs(Point.y) < region_half;
|
|
}
|
|
|
|
glm::dvec3 chunk_centre(heightmap::chunk_key const &Key)
|
|
{
|
|
auto const low{heightmap::chunk_corner(Key)};
|
|
return {low.x + heightmap::chunk_size * 0.5, 0.0, low.y + heightmap::chunk_size * 0.5};
|
|
}
|
|
|
|
// index range of the grid points (spacing Step from Origin, Count points) within [Low, High]
|
|
std::pair<int, int> index_range(double const Origin, double const Step, int const Count, double const Low, double const High)
|
|
{
|
|
auto const first{std::max(0, static_cast<int>(std::ceil((Low - Origin) / Step - 1e-9)))};
|
|
auto const last{std::min(Count - 1, static_cast<int>(std::floor((High - Origin) / Step + 1e-9)))};
|
|
return {first, last};
|
|
}
|
|
|
|
double falloff(double const Distance, double const Radius)
|
|
{
|
|
// smooth cosine falloff: full strength at the centre, zero at the rim
|
|
return 0.5 * (std::cos(kPi * Distance / Radius) + 1.0);
|
|
}
|
|
|
|
void erase_shape(std::vector<scene::shape_node> &Shapes, gfx::geometry_handle const &Geometry)
|
|
{
|
|
for (auto it = Shapes.begin(); it != Shapes.end(); ++it)
|
|
{
|
|
auto const &geometry{it->data().geometry};
|
|
if (geometry.bank == Geometry.bank && geometry.chunk == Geometry.chunk)
|
|
{
|
|
Shapes.erase(it);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
terrain_streamer::~terrain_streamer()
|
|
{
|
|
stop_workers();
|
|
}
|
|
|
|
std::string terrain_streamer::pack_path(heightmap::pack_key const &Key) const
|
|
{
|
|
return m_directory + heightmap::pack_file_name(Key);
|
|
}
|
|
|
|
bool terrain_streamer::open(std::string const &Name)
|
|
{
|
|
close();
|
|
m_name = Name;
|
|
m_directory = "terrain/" + Name + "/";
|
|
m_manifest = heightmap::manifest();
|
|
m_manifestchanged = false;
|
|
auto const manifestpath{m_directory + "terrain.yaml"};
|
|
std::error_code ec;
|
|
if (std::filesystem::exists(manifestpath, ec))
|
|
{
|
|
std::string error;
|
|
if (false == heightmap::load_manifest(manifestpath, m_manifest, &error))
|
|
ErrorLog("Bad file: heightmap terrain description \"" + manifestpath + "\": " + error, logtype::file);
|
|
}
|
|
else if (false == std::filesystem::exists(m_directory, ec))
|
|
WriteLog("Heightmap terrain \"" + m_directory + "\" has no files yet", logtype::generic);
|
|
m_active = true;
|
|
m_waterchanged = true;
|
|
start_workers();
|
|
WriteLog("Heightmap terrain: " + m_directory, logtype::generic);
|
|
return true;
|
|
}
|
|
|
|
void terrain_streamer::close()
|
|
{
|
|
stop_workers();
|
|
if (simulation::Region != nullptr)
|
|
{
|
|
for (auto &entry : m_chunks)
|
|
undraw(*entry.second);
|
|
clear_water(true);
|
|
}
|
|
clear_water(false);
|
|
m_chunks.clear();
|
|
m_packs.clear();
|
|
m_loading.clear();
|
|
m_removed.clear();
|
|
m_settled = false;
|
|
m_palette.clear();
|
|
m_recording = 0;
|
|
m_change = change();
|
|
m_active = false;
|
|
m_name.clear();
|
|
m_directory.clear();
|
|
}
|
|
|
|
void terrain_streamer::reset()
|
|
{
|
|
// the sections the shapes were put in are gone with the old region, they aren't to be touched
|
|
stop_workers();
|
|
for (auto &entry : m_chunks)
|
|
{
|
|
entry.second->m_section = nullptr;
|
|
if (entry.second->m_material != null_handle && GfxRenderer)
|
|
GfxRenderer->Terrain_Release(entry.second->m_material);
|
|
}
|
|
for (auto &slot : m_slots)
|
|
slot.used = false;
|
|
m_chunks.clear();
|
|
m_packs.clear();
|
|
m_loading.clear();
|
|
m_removed.clear();
|
|
m_settled = false;
|
|
m_palette.clear();
|
|
clear_water(false);
|
|
m_recording = 0;
|
|
m_change = change();
|
|
m_active = false;
|
|
m_name.clear();
|
|
m_directory.clear();
|
|
m_hascamera = false;
|
|
}
|
|
|
|
// workers
|
|
|
|
void terrain_streamer::start_workers()
|
|
{
|
|
if (false == m_workers.empty())
|
|
return;
|
|
m_stop = false;
|
|
for (int i = 0; i < worker_count; ++i)
|
|
m_workers.emplace_back(&terrain_streamer::worker, this);
|
|
}
|
|
|
|
void terrain_streamer::stop_workers()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock{m_mutex};
|
|
m_stop = true;
|
|
m_jobs.clear();
|
|
}
|
|
m_wakeup.notify_all();
|
|
for (auto &worker : m_workers)
|
|
if (worker.joinable())
|
|
worker.join();
|
|
m_workers.clear();
|
|
std::lock_guard<std::mutex> lock{m_mutex};
|
|
m_results.clear();
|
|
m_busy = 0;
|
|
m_stop = false;
|
|
for (auto &entry : m_chunks)
|
|
entry.second->m_building = false;
|
|
}
|
|
|
|
void terrain_streamer::submit(job Job)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock{m_mutex};
|
|
m_jobs.push_back(std::move(Job));
|
|
}
|
|
m_wakeup.notify_one();
|
|
}
|
|
|
|
void terrain_streamer::worker()
|
|
{
|
|
while (true)
|
|
{
|
|
job task;
|
|
{
|
|
std::unique_lock<std::mutex> lock{m_mutex};
|
|
m_wakeup.wait(lock, [this]() { return m_stop || false == m_jobs.empty(); });
|
|
if (m_stop)
|
|
return;
|
|
// the nearest work first
|
|
auto const next{std::min_element(m_jobs.begin(), m_jobs.end(), [](job const &A, job const &B) { return A.priority < B.priority; })};
|
|
task = std::move(*next);
|
|
m_jobs.erase(next);
|
|
++m_busy;
|
|
}
|
|
result done;
|
|
done.type = task.type;
|
|
done.pack = task.pack;
|
|
done.chunk = task.chunk;
|
|
done.generation = task.generation;
|
|
done.stride = task.stride;
|
|
done.version = task.version;
|
|
switch (task.type)
|
|
{
|
|
case job::kind::pack:
|
|
{
|
|
auto const path{pack_path(task.pack)};
|
|
std::error_code ec;
|
|
if (std::filesystem::exists(path, ec))
|
|
{
|
|
auto file{std::make_shared<heightmap::pack_file>()};
|
|
std::lock_guard<std::mutex> lock{m_filemutex};
|
|
if (file->open(path, &done.error))
|
|
done.file = file;
|
|
}
|
|
break;
|
|
}
|
|
case job::kind::chunk:
|
|
{
|
|
std::lock_guard<std::mutex> lock{m_filemutex};
|
|
done.data = task.file->read(task.chunk, &done.error);
|
|
break;
|
|
}
|
|
case job::kind::mesh:
|
|
{
|
|
auto const &neighbours{task.neighbours};
|
|
heightmap::build_mesh(*task.data, task.stride, task.origin,
|
|
[&neighbours](double const X, double const Z, double &Height) {
|
|
for (auto const &neighbour : neighbours)
|
|
if (neighbour != nullptr && neighbour->contains(X, Z))
|
|
{
|
|
Height = neighbour->height_at(X, Z);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
done.mesh);
|
|
break;
|
|
}
|
|
}
|
|
std::lock_guard<std::mutex> lock{m_mutex};
|
|
m_results.push_back(std::move(done));
|
|
--m_busy;
|
|
}
|
|
}
|
|
|
|
void terrain_streamer::process(result &Result)
|
|
{
|
|
switch (Result.type)
|
|
{
|
|
case job::kind::pack:
|
|
{
|
|
auto &pack{m_packs[Result.pack]};
|
|
if (Result.generation != pack.generation)
|
|
break; // the file was written meanwhile, and is known already
|
|
if (Result.file != nullptr)
|
|
{
|
|
pack.file = std::move(Result.file);
|
|
pack.status = pack_state::state::ready;
|
|
}
|
|
else
|
|
{
|
|
if (false == Result.error.empty())
|
|
ErrorLog("Bad file: " + Result.error, logtype::file);
|
|
pack.status = pack_state::state::missing;
|
|
}
|
|
break;
|
|
}
|
|
case job::kind::chunk:
|
|
{
|
|
m_loading.erase(Result.chunk);
|
|
auto const pack{m_packs.find(heightmap::pack_of(Result.chunk))};
|
|
if (pack == m_packs.end() || pack->second.generation != Result.generation)
|
|
break; // read from a file which was written anew since
|
|
if (Result.data == nullptr)
|
|
{
|
|
ErrorLog("Bad file: " + Result.error, logtype::file);
|
|
break;
|
|
}
|
|
if (m_chunks.count(Result.chunk) > 0 || m_removed.count(Result.chunk) > 0)
|
|
break;
|
|
auto chunk{std::make_unique<editor_terrain>(std::move(Result.data))};
|
|
auto const [low, high]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())};
|
|
auto const corner{chunk->data().corner()};
|
|
chunk->m_low = {corner.x, *low, corner.y};
|
|
chunk->m_high = {corner.x + heightmap::chunk_size, *high, corner.y + heightmap::chunk_size};
|
|
m_chunks.emplace(Result.chunk, std::move(chunk));
|
|
touch_neighbours(Result.chunk);
|
|
break;
|
|
}
|
|
case job::kind::mesh:
|
|
{
|
|
auto const lookup{m_chunks.find(Result.chunk)};
|
|
if (lookup == m_chunks.end())
|
|
break;
|
|
auto &chunk{*lookup->second};
|
|
chunk.m_building = false;
|
|
if (Result.version < chunk.m_builtversion)
|
|
break;
|
|
upload(chunk, Result.mesh, Result.stride);
|
|
chunk.m_builtversion = Result.version;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// drawing
|
|
|
|
material_handle terrain_streamer::palette_material(std::uint16_t const Layer)
|
|
{
|
|
auto const index{Layer < m_manifest.layers.size() ? Layer : std::uint16_t{0}};
|
|
auto const lookup{m_palette.find(index)};
|
|
if (lookup != m_palette.end())
|
|
return lookup->second;
|
|
auto const material{m_manifest.layers.empty() ? null_handle : GfxRenderer->Fetch_Material(m_manifest.layers[index].material)};
|
|
m_palette.emplace(index, material);
|
|
return material;
|
|
}
|
|
|
|
void terrain_streamer::update_material(editor_terrain &Chunk)
|
|
{
|
|
auto const &data{Chunk.data()};
|
|
std::vector<gfx::terrain_layer> layers;
|
|
auto const count{std::max<std::size_t>(1, data.layer_count())};
|
|
for (std::size_t slot = 0; slot < count; ++slot)
|
|
{
|
|
auto const layer{data.layers[slot] == heightmap::no_layer ? std::uint16_t{0} : data.layers[slot]};
|
|
auto const size{layer < m_manifest.layers.size() ? m_manifest.layers[layer].size : 0.f};
|
|
auto const rotation{layer < m_manifest.layers.size() ? m_manifest.layers[layer].rotation : 0.f};
|
|
layers.push_back({palette_material(layer), size, rotation});
|
|
}
|
|
auto const corner{data.corner()};
|
|
auto const packcorner{heightmap::pack_corner(heightmap::pack_of(data.key))};
|
|
glm::vec3 const placement{static_cast<float>(corner.x - packcorner.x), static_cast<float>(corner.y - packcorner.y), static_cast<float>(heightmap::chunk_size)};
|
|
auto const painted{data.paint > 0 && count > 1};
|
|
auto const material{GfxRenderer->Terrain_Material(Chunk.m_material, layers, painted ? data.paint : 0, painted ? data.weights.data() : nullptr, placement)};
|
|
if (Chunk.m_material != null_handle && Chunk.m_material != material)
|
|
GfxRenderer->Terrain_Release(Chunk.m_material);
|
|
Chunk.m_material = material;
|
|
Chunk.m_paintedversion = Chunk.m_paintversion;
|
|
}
|
|
|
|
void terrain_streamer::upload(editor_terrain &Chunk, heightmap::mesh &Mesh, int const Stride)
|
|
{
|
|
if (simulation::Region == nullptr || Mesh.vertices.empty())
|
|
return;
|
|
auto const cells{Chunk.cells()};
|
|
auto const triangles{Mesh.indices.size() / 3};
|
|
gfx::userdata_array nouserdata;
|
|
|
|
// the mesh goes to a geometry slot made for its grid; a slot of the same kind is refilled in place
|
|
auto fits = [&](geometry_slot const &Slot) { return Slot.cells == cells && Slot.stride == Stride; };
|
|
if (Chunk.m_slot < 0 || false == fits(m_slots[Chunk.m_slot]))
|
|
{
|
|
if (Chunk.m_slot >= 0)
|
|
m_slots[Chunk.m_slot].used = false;
|
|
Chunk.m_slot = -1;
|
|
for (std::size_t i = 0; i < m_slots.size(); ++i)
|
|
if (false == m_slots[i].used && fits(m_slots[i]))
|
|
{
|
|
Chunk.m_slot = static_cast<int>(i);
|
|
break;
|
|
}
|
|
if (Chunk.m_slot < 0)
|
|
{
|
|
geometry_slot slot;
|
|
slot.bank = GfxRenderer->Create_Bank();
|
|
slot.cells = cells;
|
|
slot.stride = Stride;
|
|
slot.geometry = GfxRenderer->Insert(Mesh.indices, Mesh.vertices, nouserdata, slot.bank, GL_TRIANGLES);
|
|
if (slot.geometry.chunk == 0)
|
|
{
|
|
// the shape can't keep the geometry it had, it's given back already
|
|
undraw(Chunk);
|
|
return;
|
|
}
|
|
slot.used = true;
|
|
Chunk.m_slot = static_cast<int>(m_slots.size());
|
|
m_slotofgeometry[geometry_key(slot.geometry)] = Chunk.m_slot;
|
|
m_slots.push_back(slot);
|
|
}
|
|
else
|
|
{
|
|
m_slots[Chunk.m_slot].used = true;
|
|
GfxRenderer->Replace(Mesh.vertices, nouserdata, m_slots[Chunk.m_slot].geometry, GL_TRIANGLES);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GfxRenderer->Replace(Mesh.vertices, nouserdata, m_slots[Chunk.m_slot].geometry, GL_TRIANGLES);
|
|
}
|
|
auto const &slot{m_slots[Chunk.m_slot]};
|
|
|
|
if (Chunk.m_material == null_handle || Chunk.m_paintedversion != Chunk.m_paintversion)
|
|
update_material(Chunk);
|
|
|
|
// the shape gets just two bounding vertices: they give it its area, and leave no triangles behind for code which
|
|
// reads the source vertices of the section shapes; the terrain answers questions about the ground itself
|
|
auto const centre{chunk_centre(Chunk.key())};
|
|
auto §ion{simulation::Region->section(centre)};
|
|
// the section makes the geometry of its own shapes once, before it's drawn; done first, it leaves ours alone
|
|
section.create_geometry();
|
|
auto const low{Mesh.low};
|
|
auto const high{Mesh.high};
|
|
std::vector<world_vertex> bounds(2);
|
|
bounds[0].position = low;
|
|
bounds[1].position = high;
|
|
scene::shape_node shape;
|
|
shape.make_terrain(Chunk.m_material, std::move(bounds), section.m_area.center);
|
|
shape.geometry(slot.geometry);
|
|
|
|
auto replaced{false};
|
|
if (Chunk.m_section != nullptr)
|
|
{
|
|
for (auto &existing : Chunk.m_section->m_shapes)
|
|
{
|
|
auto const &geometry{existing.data().geometry};
|
|
if (geometry.bank == Chunk.m_geometry.bank && geometry.chunk == Chunk.m_geometry.chunk)
|
|
{
|
|
existing = shape;
|
|
replaced = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (false == replaced)
|
|
section.m_shapes.emplace_back(shape);
|
|
// the section reaches far enough for the chunk not to be culled at its edges
|
|
section.m_area.radius = std::max(section.m_area.radius, static_cast<float>(glm::length(section.m_area.center - Mesh.centre) + Mesh.radius));
|
|
Chunk.m_section = §ion;
|
|
Chunk.m_geometry = slot.geometry;
|
|
Chunk.m_triangles = triangles;
|
|
Chunk.m_stride = Stride;
|
|
Chunk.m_low = low;
|
|
Chunk.m_high = high;
|
|
}
|
|
|
|
void terrain_streamer::undraw(editor_terrain &Chunk)
|
|
{
|
|
if (Chunk.m_section != nullptr && simulation::Region != nullptr)
|
|
erase_shape(Chunk.m_section->m_shapes, Chunk.m_geometry);
|
|
if (Chunk.m_slot >= 0)
|
|
m_slots[Chunk.m_slot].used = false;
|
|
if (Chunk.m_material != null_handle && GfxRenderer)
|
|
GfxRenderer->Terrain_Release(Chunk.m_material);
|
|
Chunk.m_slot = -1;
|
|
Chunk.m_material = null_handle;
|
|
Chunk.m_section = nullptr;
|
|
Chunk.m_geometry = gfx::geometry_handle{0, 0};
|
|
Chunk.m_triangles = 0;
|
|
Chunk.m_stride = 0;
|
|
Chunk.m_builtversion = 0;
|
|
Chunk.m_paintedversion = 0;
|
|
}
|
|
|
|
heightmap::chunk_ptr terrain_streamer::neighbour(chunk_key const &Key) const
|
|
{
|
|
auto const lookup{m_chunks.find(Key)};
|
|
return lookup != m_chunks.end() ? lookup->second->shared() : nullptr;
|
|
}
|
|
|
|
void terrain_streamer::touch_neighbours(chunk_key const &Key)
|
|
{
|
|
for (auto const &offset : {chunk_key{-1, 0}, chunk_key{1, 0}, chunk_key{0, -1}, chunk_key{0, 1}})
|
|
{
|
|
auto const lookup{m_chunks.find({Key.first + offset.first, Key.second + offset.second})};
|
|
if (lookup != m_chunks.end())
|
|
lookup->second->m_neighbours = true;
|
|
}
|
|
}
|
|
|
|
int terrain_streamer::wanted_stride(editor_terrain const &Chunk, glm::dvec3 const &Camera) const
|
|
{
|
|
// distance from the camera to the box of the chunk
|
|
glm::dvec3 const offset{std::max({Chunk.m_low.x - Camera.x, 0.0, Camera.x - Chunk.m_high.x}), std::max({Chunk.m_low.y - Camera.y, 0.0, Camera.y - Chunk.m_high.y}),
|
|
std::max({Chunk.m_low.z - Camera.z, 0.0, Camera.z - Chunk.m_high.z})};
|
|
auto const distance{m_pixel > 0.0 ? m_pixel * pixel_distance : glm::length(offset)};
|
|
auto const spacing{Chunk.data().spacing()};
|
|
auto const limit{heightmap::max_stride(Chunk.cells())};
|
|
auto const stride_for = [&](double const Distance) {
|
|
auto const edge{Distance / m_detail};
|
|
int stride{1};
|
|
while (stride * 2 <= limit && stride * 2 * spacing <= edge)
|
|
stride *= 2;
|
|
return stride;
|
|
};
|
|
// a margin around the distances the stride changes at, so a camera standing at one doesn't flip it to and fro
|
|
if (Chunk.m_stride > 0)
|
|
{
|
|
auto const finer{stride_for(distance * 0.85)};
|
|
auto const coarser{stride_for(distance * 1.15)};
|
|
if (Chunk.m_stride >= finer && Chunk.m_stride <= coarser)
|
|
return Chunk.m_stride;
|
|
}
|
|
return stride_for(distance);
|
|
}
|
|
|
|
void terrain_streamer::request_mesh(editor_terrain &Chunk, int const Stride, double const Priority)
|
|
{
|
|
auto const centre{chunk_centre(Chunk.key())};
|
|
job task;
|
|
task.type = job::kind::mesh;
|
|
task.chunk = Chunk.key();
|
|
task.data = Chunk.shared();
|
|
auto const key{Chunk.key()};
|
|
task.neighbours = {neighbour({key.first - 1, key.second}), neighbour({key.first + 1, key.second}), neighbour({key.first, key.second - 1}), neighbour({key.first, key.second + 1})};
|
|
task.stride = Stride;
|
|
task.origin = simulation::Region->section(centre).m_area.center;
|
|
task.version = Chunk.m_shapeversion;
|
|
task.priority = Priority;
|
|
Chunk.m_building = true;
|
|
Chunk.m_neighbours = false;
|
|
submit(std::move(task));
|
|
}
|
|
|
|
void terrain_streamer::update(glm::dvec3 const &Camera)
|
|
{
|
|
if (false == m_active || simulation::Region == nullptr)
|
|
return;
|
|
|
|
// where the camera is heading
|
|
auto const now{steady_seconds()};
|
|
if (m_hascamera)
|
|
{
|
|
auto const elapsed{now - m_cameratime};
|
|
if (elapsed > 1e-3)
|
|
{
|
|
auto velocity{(Camera - m_camera) / elapsed};
|
|
if (glm::length(velocity) > 300.0)
|
|
velocity = glm::dvec3{0.0}; // a jump, not a movement
|
|
m_velocity = glm::mix(m_velocity, velocity, std::min(1.0, elapsed * 2.0));
|
|
}
|
|
}
|
|
m_camera = Camera;
|
|
m_cameratime = now;
|
|
m_hascamera = true;
|
|
auto ahead{m_velocity * look_ahead};
|
|
if (glm::length(ahead) > look_ahead_limit)
|
|
ahead *= look_ahead_limit / glm::length(ahead);
|
|
auto const target{Camera + ahead};
|
|
|
|
if (m_waterchanged)
|
|
build_water();
|
|
|
|
// finished work, within a few milliseconds
|
|
while (steady_seconds() - now < upload_budget)
|
|
{
|
|
result done;
|
|
{
|
|
std::lock_guard<std::mutex> lock{m_mutex};
|
|
if (m_results.empty())
|
|
break;
|
|
done = std::move(m_results.front());
|
|
m_results.pop_front();
|
|
}
|
|
process(done);
|
|
m_settled = false;
|
|
}
|
|
|
|
// the top view of the tracks wants the terrain as far as the corners of the screen, as fine as its pixels need
|
|
auto const top{Global.EditorOrtho && Global.editor_tracks};
|
|
auto const aspect{Global.window_size.x / std::max(1.0, static_cast<double>(Global.window_size.y))};
|
|
auto const reach{top ? Global.EditorOrthoExtent * std::sqrt(1.0 + aspect * aspect) : 0.0};
|
|
m_pixel = top ? 2.0 * Global.EditorOrthoExtent / std::max(1, Global.window_size.y) : 0.0;
|
|
|
|
// chunks wanted around the camera and around where it's heading
|
|
auto const range{std::max({m_radius * heightmap::chunk_size, static_cast<double>(Global.BaseDrawRange) * std::max(1.0, static_cast<double>(Global.fDistanceFactor)), reach}) + heightmap::chunk_size * 0.5};
|
|
auto const radius{std::min(top ? top_radius : 64, static_cast<int>(std::ceil(range / heightmap::chunk_size)))};
|
|
if (m_ringradius < radius)
|
|
{
|
|
m_ring.clear();
|
|
for (int dz = -radius; dz <= radius; ++dz)
|
|
for (int dx = -radius; dx <= radius; ++dx)
|
|
if (dx * dx + dz * dz <= (radius + 1) * (radius + 1))
|
|
m_ring.emplace_back(dx, dz);
|
|
std::sort(m_ring.begin(), m_ring.end(), [](chunk_key const &A, chunk_key const &B) { return A.first * A.first + A.second * A.second < B.first * B.first + B.second * B.second; });
|
|
m_ringradius = radius;
|
|
}
|
|
auto const here{heightmap::chunk_at(Camera.x, Camera.z)};
|
|
auto const there{heightmap::chunk_at(target.x, target.z)};
|
|
std::vector<chunk_key> centres{here};
|
|
if (there != here)
|
|
centres.push_back(there);
|
|
// a walk which asked for nothing isn't made again until something comes or the camera moves to another chunk
|
|
if (m_settled && (here != m_walkhere || there != m_walkthere || radius != m_walkradius || now - m_walktime > settled_walk))
|
|
m_settled = false;
|
|
if (false == m_settled)
|
|
{
|
|
m_walkhere = here;
|
|
m_walkthere = there;
|
|
m_walkradius = radius;
|
|
m_walktime = now;
|
|
m_settled = m_loading.empty();
|
|
}
|
|
else
|
|
centres.clear();
|
|
auto const walked{(radius + 1) * (radius + 1)};
|
|
for (auto const ¢re : centres)
|
|
{
|
|
for (auto const &offset : m_ring)
|
|
{
|
|
if (offset.first * offset.first + offset.second * offset.second > walked)
|
|
break;
|
|
if (m_loading.size() >= max_loading)
|
|
{
|
|
m_settled = false;
|
|
break;
|
|
}
|
|
chunk_key const key{centre.first + offset.first, centre.second + offset.second};
|
|
if (m_chunks.count(key) > 0 || m_loading.count(key) > 0 || m_removed.count(key) > 0)
|
|
continue;
|
|
auto const packkey{heightmap::pack_of(key)};
|
|
auto &pack{m_packs[packkey]};
|
|
auto const distance{std::sqrt(static_cast<double>(offset.first * offset.first + offset.second * offset.second))};
|
|
if (pack.status == pack_state::state::unknown)
|
|
{
|
|
pack.status = pack_state::state::loading;
|
|
job task;
|
|
task.type = job::kind::pack;
|
|
task.pack = packkey;
|
|
task.generation = pack.generation;
|
|
task.priority = distance;
|
|
submit(std::move(task));
|
|
m_settled = false;
|
|
continue;
|
|
}
|
|
if (pack.status == pack_state::state::loading)
|
|
m_settled = false;
|
|
if (pack.status != pack_state::state::ready || false == pack.file->has(key))
|
|
continue;
|
|
job task;
|
|
task.type = job::kind::chunk;
|
|
task.chunk = key;
|
|
task.pack = packkey;
|
|
task.file = pack.file;
|
|
task.generation = pack.generation;
|
|
task.priority = distance;
|
|
m_loading.insert(key);
|
|
submit(std::move(task));
|
|
m_settled = false;
|
|
}
|
|
}
|
|
|
|
// chunks gone out of range are dropped, except edited ones, which wait in memory to be saved
|
|
auto const outside = [&](chunk_key const &Key, int const Margin) {
|
|
auto const within = [&](chunk_key const &Centre) {
|
|
auto const dx{Key.first - Centre.first}, dz{Key.second - Centre.second};
|
|
return dx * dx + dz * dz <= (radius + Margin) * (radius + Margin);
|
|
};
|
|
return false == within(here) && false == within(there);
|
|
};
|
|
for (auto it = m_chunks.begin(); it != m_chunks.end();)
|
|
{
|
|
auto &chunk{*it->second};
|
|
if (outside(it->first, 2))
|
|
{
|
|
if (chunk.m_section != nullptr || chunk.m_slot >= 0)
|
|
undraw(chunk);
|
|
if (false == chunk.modified() && false == chunk.m_building)
|
|
{
|
|
it = m_chunks.erase(it);
|
|
continue;
|
|
}
|
|
}
|
|
++it;
|
|
}
|
|
|
|
// meshes of the grid each chunk's distance asks for, the nearest first
|
|
std::size_t meshing{0};
|
|
{
|
|
std::lock_guard<std::mutex> lock{m_mutex};
|
|
meshing = m_busy;
|
|
for (auto const &queued : m_jobs)
|
|
if (queued.type == job::kind::mesh)
|
|
++meshing;
|
|
}
|
|
std::vector<std::pair<double, editor_terrain *>> wanted;
|
|
int materials{0};
|
|
for (auto &entry : m_chunks)
|
|
{
|
|
auto &chunk{*entry.second};
|
|
if (outside(entry.first, 1))
|
|
continue;
|
|
if (false == in_region(glm::dvec2{chunk_centre(entry.first).x, chunk_centre(entry.first).z}))
|
|
continue;
|
|
// paint changes go to the material straight away
|
|
if (chunk.m_section != nullptr && chunk.m_paintedversion != chunk.m_paintversion && materials < materials_per_frame)
|
|
{
|
|
update_material(chunk);
|
|
upload_shape_material(chunk);
|
|
++materials;
|
|
}
|
|
if (chunk.m_building)
|
|
continue;
|
|
auto const stride{wanted_stride(chunk, Camera)};
|
|
if (chunk.m_stride != stride || chunk.m_builtversion != chunk.m_shapeversion || chunk.m_neighbours)
|
|
{
|
|
auto const centre{chunk_centre(entry.first)};
|
|
wanted.emplace_back(glm::length(glm::dvec2{centre.x - Camera.x, centre.z - Camera.z}), &chunk);
|
|
}
|
|
}
|
|
std::sort(wanted.begin(), wanted.end(), [](auto const &A, auto const &B) { return A.first < B.first; });
|
|
for (auto const &entry : wanted)
|
|
{
|
|
if (meshing >= max_meshing)
|
|
break;
|
|
request_mesh(*entry.second, wanted_stride(*entry.second, Camera), entry.first / heightmap::chunk_size);
|
|
++meshing;
|
|
}
|
|
}
|
|
|
|
void terrain_streamer::upload_shape_material(editor_terrain &Chunk)
|
|
{
|
|
if (Chunk.m_section == nullptr)
|
|
return;
|
|
for (auto &existing : Chunk.m_section->m_shapes)
|
|
{
|
|
auto const &geometry{existing.data().geometry};
|
|
if (geometry.bank == Chunk.m_geometry.bank && geometry.chunk == Chunk.m_geometry.chunk)
|
|
{
|
|
// the shape is made anew with the new material, the rest of it stays
|
|
std::vector<world_vertex> bounds(2);
|
|
bounds[0].position = Chunk.m_low;
|
|
bounds[1].position = Chunk.m_high;
|
|
scene::shape_node shape;
|
|
shape.make_terrain(Chunk.m_material, std::move(bounds), existing.data().origin);
|
|
shape.geometry(Chunk.m_geometry);
|
|
existing = shape;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// queries
|
|
|
|
void terrain_streamer::collect(std::vector<editor_terrain *> &Out) const
|
|
{
|
|
for (auto const &entry : m_chunks)
|
|
Out.push_back(entry.second.get());
|
|
}
|
|
|
|
editor_terrain *terrain_streamer::terrain_at(double X, double Z) const
|
|
{
|
|
auto const lookup{m_chunks.find(heightmap::chunk_at(X, Z))};
|
|
if (lookup != m_chunks.end() && lookup->second->contains(X, Z))
|
|
return lookup->second.get();
|
|
return nullptr;
|
|
}
|
|
|
|
bool terrain_streamer::height_at(double X, double Z, double &Height) const
|
|
{
|
|
auto const *chunk{terrain_at(X, Z)};
|
|
if (chunk == nullptr)
|
|
return false;
|
|
Height = chunk->height_at(X, Z);
|
|
return true;
|
|
}
|
|
|
|
void terrain_streamer::gather_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<std::array<glm::dvec3, 3>> &Out) const
|
|
{
|
|
auto const first{heightmap::chunk_at(Min.x, Min.y)};
|
|
auto const last{heightmap::chunk_at(Max.x, Max.y)};
|
|
for (int cz = first.second; cz <= last.second; ++cz)
|
|
for (int cx = first.first; cx <= last.first; ++cx)
|
|
{
|
|
auto const lookup{m_chunks.find({cx, cz})};
|
|
if (lookup == m_chunks.end())
|
|
continue;
|
|
auto const &data{lookup->second->data()};
|
|
auto const low{data.corner()};
|
|
auto const step{data.spacing()};
|
|
// the cells overlapping the rectangle
|
|
auto const xs{index_range(low.x, step, data.cells, Min.x - step, Max.x)};
|
|
auto const zs{index_range(low.y, step, data.cells, Min.y - step, Max.y)};
|
|
auto const point = [&](int const Ix, int const Iz) { return glm::dvec3{low.x + Ix * step, data.height(Ix, Iz), low.y + Iz * step}; };
|
|
for (int iz = zs.first; iz <= zs.second && iz < data.cells; ++iz)
|
|
for (int ix = xs.first; ix <= xs.second && ix < data.cells; ++ix)
|
|
{
|
|
auto const v00{point(ix, iz)}, v10{point(ix + 1, iz)}, v01{point(ix, iz + 1)}, v11{point(ix + 1, iz + 1)};
|
|
Out.push_back({v00, v01, v10});
|
|
Out.push_back({v11, v10, v01});
|
|
}
|
|
}
|
|
}
|
|
|
|
bool terrain_streamer::owns(gfx::geometry_handle const &Geometry) const
|
|
{
|
|
if (Geometry.bank == 0 && Geometry.chunk == 0)
|
|
return false;
|
|
if (m_slotofgeometry.count(geometry_key(Geometry)) > 0)
|
|
return true;
|
|
for (auto const &water : m_watershapes)
|
|
if (water.geometry.bank == Geometry.bank && water.geometry.chunk == Geometry.chunk)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool terrain_streamer::exists(chunk_key const &Key) const
|
|
{
|
|
if (m_chunks.count(Key) > 0)
|
|
return true;
|
|
if (m_removed.count(Key) > 0)
|
|
return false;
|
|
auto const pack{m_packs.find(heightmap::pack_of(Key))};
|
|
return pack != m_packs.end() && pack->second.status == pack_state::state::ready && pack->second.file->has(Key);
|
|
}
|
|
|
|
// editing
|
|
|
|
editor_terrain *terrain_streamer::load_now(chunk_key const &Key)
|
|
{
|
|
auto const lookup{m_chunks.find(Key)};
|
|
if (lookup != m_chunks.end())
|
|
return lookup->second.get();
|
|
if (m_removed.count(Key) > 0)
|
|
return nullptr;
|
|
auto const packkey{heightmap::pack_of(Key)};
|
|
auto &pack{m_packs[packkey]};
|
|
if (pack.status != pack_state::state::ready && pack.status != pack_state::state::missing)
|
|
{
|
|
// the index is read here and now; a worker reading it meanwhile is ignored by the generation
|
|
++pack.generation;
|
|
pack.status = pack_state::state::missing;
|
|
auto const path{pack_path(packkey)};
|
|
std::error_code ec;
|
|
if (std::filesystem::exists(path, ec))
|
|
{
|
|
auto file{std::make_shared<heightmap::pack_file>()};
|
|
std::string error;
|
|
std::lock_guard<std::mutex> lock{m_filemutex};
|
|
if (file->open(path, &error))
|
|
{
|
|
pack.file = file;
|
|
pack.status = pack_state::state::ready;
|
|
}
|
|
else
|
|
ErrorLog("Bad file: " + error, logtype::file);
|
|
}
|
|
}
|
|
if (pack.status != pack_state::state::ready || false == pack.file->has(Key))
|
|
return nullptr;
|
|
std::shared_ptr<heightmap::chunk_data> data;
|
|
std::string error;
|
|
{
|
|
std::lock_guard<std::mutex> lock{m_filemutex};
|
|
data = pack.file->read(Key, &error);
|
|
}
|
|
if (data == nullptr)
|
|
{
|
|
ErrorLog("Bad file: " + error, logtype::file);
|
|
return nullptr;
|
|
}
|
|
auto chunk{std::make_unique<editor_terrain>(std::move(data))};
|
|
auto const [low, high]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())};
|
|
auto const corner{chunk->data().corner()};
|
|
chunk->m_low = {corner.x, *low, corner.y};
|
|
chunk->m_high = {corner.x + heightmap::chunk_size, *high, corner.y + heightmap::chunk_size};
|
|
auto *result{chunk.get()};
|
|
m_chunks.emplace(Key, std::move(chunk));
|
|
m_loading.erase(Key);
|
|
touch_neighbours(Key);
|
|
return result;
|
|
}
|
|
|
|
std::vector<editor_terrain *> terrain_streamer::chunks_in(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Load)
|
|
{
|
|
std::vector<editor_terrain *> result;
|
|
auto const first{heightmap::chunk_at(Min.x, Min.y)};
|
|
auto const last{heightmap::chunk_at(Max.x, Max.y)};
|
|
for (int cz = first.second; cz <= last.second; ++cz)
|
|
for (int cx = first.first; cx <= last.first; ++cx)
|
|
{
|
|
auto const lookup{m_chunks.find({cx, cz})};
|
|
if (lookup != m_chunks.end())
|
|
result.push_back(lookup->second.get());
|
|
else if (Load)
|
|
{
|
|
if (auto *chunk{load_now({cx, cz})})
|
|
result.push_back(chunk);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, float Height, std::uint16_t const Layer)
|
|
{
|
|
return add_chunk(Cx, Cz, Spacing, [Height](double, double) { return Height; }, Layer);
|
|
}
|
|
|
|
bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, std::function<float(double X, double Z)> const &Height, std::uint16_t const Layer)
|
|
{
|
|
chunk_key const key{Cx, Cz};
|
|
if (false == m_active || load_now(key) != nullptr || m_chunks.count(key) > 0)
|
|
return false;
|
|
if (false == in_region(glm::dvec2{chunk_centre(key).x, chunk_centre(key).z}))
|
|
return false;
|
|
auto data{heightmap::chunk_data::make_flat(key, Spacing, 0.f)};
|
|
data->layers[0] = Layer < m_manifest.layers.size() ? Layer : std::uint16_t{0};
|
|
auto const low{data->corner()};
|
|
auto const step{data->spacing()};
|
|
for (int iz = 0; iz <= data->cells; ++iz)
|
|
for (int ix = 0; ix <= data->cells; ++ix)
|
|
data->heights[static_cast<std::size_t>(iz) * data->side() + ix] = Height(low.x + ix * step, low.y + iz * step);
|
|
return add_chunk(std::move(data));
|
|
}
|
|
|
|
bool terrain_streamer::add_chunk(std::shared_ptr<heightmap::chunk_data> Data)
|
|
{
|
|
if (Data == nullptr || false == Data->valid())
|
|
return false;
|
|
auto const key{Data->key};
|
|
if (false == m_active || load_now(key) != nullptr || m_chunks.count(key) > 0)
|
|
return false;
|
|
if (false == in_region(glm::dvec2{chunk_centre(key).x, chunk_centre(key).z}))
|
|
return false;
|
|
for (auto &layer : Data->layers)
|
|
if (layer != heightmap::no_layer && layer >= m_manifest.layers.size())
|
|
layer = 0;
|
|
auto data{std::move(Data)};
|
|
auto const low{data->corner()};
|
|
remember(key, nullptr);
|
|
auto chunk{std::make_unique<editor_terrain>(std::move(data))};
|
|
chunk->m_modified = true;
|
|
auto const [lowest, highest]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())};
|
|
chunk->m_low = {low.x, *lowest, low.y};
|
|
chunk->m_high = {low.x + heightmap::chunk_size, *highest, low.y + heightmap::chunk_size};
|
|
auto &added{*chunk};
|
|
m_chunks.emplace(key, std::move(chunk));
|
|
m_removed.erase(key);
|
|
m_loading.erase(key);
|
|
apply_modifiers(added, low, low + glm::dvec2{heightmap::chunk_size});
|
|
touch_neighbours(key);
|
|
return true;
|
|
}
|
|
|
|
void terrain_streamer::remove_chunk(int Cx, int Cz)
|
|
{
|
|
chunk_key const key{Cx, Cz};
|
|
if (recording())
|
|
{
|
|
// the data is needed to bring the chunk back
|
|
if (auto const *chunk{load_now(key)})
|
|
remember(*chunk);
|
|
}
|
|
auto const lookup{m_chunks.find(key)};
|
|
if (lookup != m_chunks.end())
|
|
{
|
|
undraw(*lookup->second);
|
|
m_chunks.erase(lookup);
|
|
}
|
|
m_removed.insert(key);
|
|
m_loading.erase(key);
|
|
touch_neighbours(key);
|
|
}
|
|
|
|
bool terrain_streamer::spacing(int Cx, int Cz, float Spacing, int Paint)
|
|
{
|
|
auto *chunk{load_now({Cx, Cz})};
|
|
if (chunk == nullptr)
|
|
return false;
|
|
auto const ¤t{chunk->data()};
|
|
auto const repaint{Paint > 0 && current.paint > 0 && Paint != current.paint};
|
|
if (heightmap::cells_for(Spacing) == current.cells && false == repaint)
|
|
return false;
|
|
remember(*chunk);
|
|
chunk->replace(current.resampled(Spacing, Paint), true, repaint);
|
|
auto const low{chunk->data().corner()};
|
|
apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size});
|
|
touch_neighbours(chunk->key());
|
|
return true;
|
|
}
|
|
|
|
bool terrain_streamer::reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std::function<bool(double X, double Z, float &Height)> const &Shaper)
|
|
{
|
|
bool changed{false};
|
|
for (auto *chunk : chunks_in(Min, Max, true))
|
|
{
|
|
auto const ¤t{chunk->data()};
|
|
auto const low{current.corner()};
|
|
auto const step{current.spacing()};
|
|
auto const xs{index_range(low.x, step, current.side(), Min.x, Max.x)};
|
|
auto const zs{index_range(low.y, step, current.side(), Min.y, Max.y)};
|
|
if (xs.first > xs.second || zs.first > zs.second)
|
|
continue;
|
|
if (current.base.empty() || false == m_touchup)
|
|
{
|
|
// a copy is shaped, and taken only if anything changed
|
|
auto heights{current.base.empty() ? current.heights : current.base};
|
|
bool shaped{false};
|
|
for (int iz = zs.first; iz <= zs.second; ++iz)
|
|
for (int ix = xs.first; ix <= xs.second; ++ix)
|
|
shaped |= Shaper(low.x + ix * step, low.y + iz * step, heights[static_cast<std::size_t>(iz) * current.side() + ix]);
|
|
if (false == shaped)
|
|
continue;
|
|
remember(*chunk);
|
|
auto &data{chunk->edit(true, false)};
|
|
(data.base.empty() ? data.heights : data.base) = std::move(heights);
|
|
if (false == data.base.empty())
|
|
apply_modifiers(*chunk, Min, Max);
|
|
changed = true;
|
|
continue;
|
|
}
|
|
// the terrain as it's seen is shaped. where a modifier shapes the ground (or a touch-up is there already) the change
|
|
// is a touch-up over it, elsewhere it's the ground under the modifiers, which may then reach further
|
|
auto heights{current.heights};
|
|
auto base{current.base};
|
|
auto adjust{current.adjust.empty() ? std::vector<float>(heights.size(), 0.f) : current.adjust};
|
|
bool shaped{false}, touched{false};
|
|
for (int iz = zs.first; iz <= zs.second; ++iz)
|
|
for (int ix = xs.first; ix <= xs.second; ++ix)
|
|
{
|
|
auto const index{static_cast<std::size_t>(iz) * current.side() + ix};
|
|
auto height{heights[index]};
|
|
if (false == Shaper(low.x + ix * step, low.y + iz * step, height))
|
|
continue;
|
|
shaped = true;
|
|
auto const change{height - heights[index]};
|
|
auto const shapedbymodifier{std::abs(heights[index] - adjust[index] - base[index]) > 0.0005f};
|
|
if (shapedbymodifier || adjust[index] != 0.f)
|
|
{
|
|
adjust[index] += change;
|
|
touched = true;
|
|
}
|
|
else
|
|
base[index] += change;
|
|
heights[index] = height;
|
|
}
|
|
if (false == shaped)
|
|
continue;
|
|
remember(*chunk);
|
|
auto &data{chunk->edit(true, false)};
|
|
data.base = std::move(base);
|
|
if (touched)
|
|
data.adjust = std::move(adjust);
|
|
data.heights = std::move(heights);
|
|
apply_modifiers(*chunk, Min, Max);
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
bool terrain_streamer::restore_stitching(double X, double Z, double Radius, double Amount)
|
|
{
|
|
if (Radius <= 0.0 || Amount <= 0.0)
|
|
return false;
|
|
glm::dvec2 const low{X - Radius, Z - Radius}, high{X + Radius, Z + Radius};
|
|
bool changed{false};
|
|
for (auto *chunk : chunks_in(low, high, false))
|
|
{
|
|
auto const ¤t{chunk->data()};
|
|
if (current.adjust.empty())
|
|
continue;
|
|
auto const corner{current.corner()};
|
|
auto const step{current.spacing()};
|
|
auto const xs{index_range(corner.x, step, current.side(), low.x, high.x)};
|
|
auto const zs{index_range(corner.y, step, current.side(), low.y, high.y)};
|
|
auto adjust{current.adjust};
|
|
bool restored{false};
|
|
for (int iz = zs.first; iz <= zs.second; ++iz)
|
|
for (int ix = xs.first; ix <= xs.second; ++ix)
|
|
{
|
|
auto const vx{corner.x + ix * step}, vz{corner.y + iz * step};
|
|
auto const distance{std::sqrt((vx - X) * (vx - X) + (vz - Z) * (vz - Z))};
|
|
auto &value{adjust[static_cast<std::size_t>(iz) * current.side() + ix]};
|
|
if (distance > Radius || value == 0.f)
|
|
continue;
|
|
value *= static_cast<float>(1.0 - std::min(1.0, Amount * falloff(distance, Radius)));
|
|
// what's below the step of the stored heights is gone
|
|
if (std::abs(value) < heightmap::height_step * 0.5)
|
|
value = 0.f;
|
|
restored = true;
|
|
}
|
|
if (false == restored)
|
|
continue;
|
|
remember(*chunk);
|
|
auto &data{chunk->edit(true, false)};
|
|
if (std::all_of(adjust.begin(), adjust.end(), [](float const Value) { return Value == 0.f; }))
|
|
data.adjust.clear();
|
|
else
|
|
data.adjust = std::move(adjust);
|
|
apply_modifiers(*chunk, low, high);
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
std::size_t terrain_streamer::touched_up() const
|
|
{
|
|
std::size_t count{0};
|
|
for (auto const &entry : m_chunks)
|
|
{
|
|
auto const &adjust{entry.second->data().adjust};
|
|
count += static_cast<std::size_t>(std::count_if(adjust.begin(), adjust.end(), [](float const Value) { return Value != 0.f; }));
|
|
}
|
|
return count;
|
|
}
|
|
|
|
bool terrain_streamer::sculpt(double X, double Z, double Radius, double Strength)
|
|
{
|
|
if (Radius <= 0.0)
|
|
return false;
|
|
return reshape(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, [&](double const Vx, double const Vz, float &Height) {
|
|
auto const distance{std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z))};
|
|
if (distance > Radius)
|
|
return false;
|
|
Height += static_cast<float>(Strength * falloff(distance, Radius));
|
|
return true;
|
|
});
|
|
}
|
|
|
|
bool terrain_streamer::smooth(double X, double Z, double Radius, double Amount)
|
|
{
|
|
if (Radius <= 0.0 || Amount <= 0.0)
|
|
return false;
|
|
glm::dvec2 const low{X - Radius, Z - Radius}, high{X + Radius, Z + Radius};
|
|
// every chunk the brush touches is smoothed from the same ground, so the edges they share stay together
|
|
std::vector<std::pair<editor_terrain const *, std::vector<float>>> before;
|
|
for (auto *chunk : chunks_in(low - glm::dvec2{6.0}, high + glm::dvec2{6.0}, false))
|
|
before.emplace_back(chunk, (chunk->data().base.empty() || m_touchup) ? chunk->heights() : chunk->data().base);
|
|
auto const ground = [&](double const Gx, double const Gz, double const Fallback) {
|
|
for (auto const &entry : before)
|
|
if (entry.first->contains(Gx, Gz))
|
|
return entry.first->height_in(entry.second, Gx, Gz);
|
|
return Fallback;
|
|
};
|
|
auto const finest{std::accumulate(before.begin(), before.end(), heightmap::chunk_size, [](double Value, auto const &Entry) { return std::min(Value, Entry.first->data().spacing()); })};
|
|
return reshape(low, high, [&](double const Vx, double const Vz, float &Height) {
|
|
auto const distance{std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z))};
|
|
if (distance > Radius)
|
|
return false;
|
|
auto const own{static_cast<double>(Height)};
|
|
auto const step{finest};
|
|
// the neighbours a cell away, read from the ground as it was before the stroke, whichever chunk holds them
|
|
auto const average{(ground(Vx - step, Vz, own) + ground(Vx + step, Vz, own) + ground(Vx, Vz - step, own) + ground(Vx, Vz + step, own) + own * 4.0) / 8.0};
|
|
auto const smoothed{own + (average - own) * std::min(1.0, Amount * falloff(distance, Radius))};
|
|
if (std::abs(smoothed - own) < 1e-5)
|
|
return false;
|
|
Height = static_cast<float>(smoothed);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
bool terrain_streamer::level(double X, double Z, double Radius, double Target, double Amount, int const Mode)
|
|
{
|
|
if (Radius <= 0.0 || Amount <= 0.0)
|
|
return false;
|
|
return reshape(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, [&](double const Vx, double const Vz, float &Height) {
|
|
auto const distance{std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z))};
|
|
if (distance > Radius)
|
|
return false;
|
|
auto const own{static_cast<double>(Height)};
|
|
if ((Mode == 1 && own >= Target) || (Mode == 2 && own <= Target))
|
|
return false;
|
|
// the middle of the brush gets there, the rim follows less; what's within the step of the stored heights is there already
|
|
auto const levelled{own + (Target - own) * std::min(1.0, Amount * falloff(distance, Radius))};
|
|
auto const result{std::abs(Target - levelled) < heightmap::height_step ? Target : levelled};
|
|
if (std::abs(result - own) < 1e-5)
|
|
return false;
|
|
Height = static_cast<float>(result);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
bool terrain_streamer::paint(double X, double Z, double Radius, double Strength, std::uint16_t Layer)
|
|
{
|
|
if (Radius <= 0.0 || Strength <= 0.0)
|
|
return false;
|
|
return paint_area(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, Layer, false, [&](double const Px, double const Pz) {
|
|
auto const distance{std::sqrt((Px - X) * (Px - X) + (Pz - Z) * (Pz - Z))};
|
|
return distance > Radius ? 0.0 : Strength * falloff(distance, Radius);
|
|
});
|
|
}
|
|
|
|
bool terrain_streamer::fill(std::vector<glm::dvec2> const &Outline, double Feather, std::uint16_t Layer, double Strength)
|
|
{
|
|
if (Outline.size() < 3 || Strength <= 0.0)
|
|
return false;
|
|
Feather = std::max(0.0, Feather);
|
|
glm::dvec2 low{std::numeric_limits<double>::max()}, high{-std::numeric_limits<double>::max()};
|
|
for (auto const &point : Outline)
|
|
{
|
|
low = glm::min(low, point);
|
|
high = glm::max(high, point);
|
|
}
|
|
// the blend runs across the outline, half of it inside
|
|
auto const margin{Feather * 0.5};
|
|
return paint_area(low - glm::dvec2{margin}, high + glm::dvec2{margin}, Layer, true, [&](double const Px, double const Pz) {
|
|
glm::dvec2 const point{Px, Pz};
|
|
bool inside{false};
|
|
auto nearest{std::numeric_limits<double>::max()};
|
|
for (std::size_t i = 0, j = Outline.size() - 1; i < Outline.size(); j = i++)
|
|
{
|
|
auto const &a{Outline[i]};
|
|
auto const &b{Outline[j]};
|
|
if ((a.y > Pz) != (b.y > Pz) && Px < (b.x - a.x) * (Pz - a.y) / (b.y - a.y) + a.x)
|
|
inside = !inside;
|
|
if (Feather > 0.0)
|
|
{
|
|
auto const run{b - a};
|
|
auto const length{glm::dot(run, run)};
|
|
auto const t{length > 1e-12 ? std::clamp(glm::dot(point - a, run) / length, 0.0, 1.0) : 0.0};
|
|
nearest = std::min(nearest, glm::length(point - (a + run * t)));
|
|
}
|
|
}
|
|
if (Feather <= 0.0)
|
|
return inside ? Strength : 0.0;
|
|
// signed distance from the outline, inside positive, led smoothly through the blend
|
|
auto const t{std::clamp(((inside ? nearest : -nearest) + margin) / Feather, 0.0, 1.0)};
|
|
return Strength * t * t * (3.0 - 2.0 * t);
|
|
});
|
|
}
|
|
|
|
bool terrain_streamer::paint_area(glm::dvec2 const &Min, glm::dvec2 const &Max, std::uint16_t const Layer, bool const Load, std::function<double(double X, double Z)> const &Amount)
|
|
{
|
|
bool changed{false};
|
|
for (auto *chunk : chunks_in(Min, Max, Load))
|
|
{
|
|
auto const ¤t{chunk->data()};
|
|
auto const samples{current.paint > 0 ? current.paint : heightmap::default_paint_samples};
|
|
auto const step{heightmap::chunk_size / samples};
|
|
auto const low{current.corner()};
|
|
auto const xs{index_range(low.x, step, samples + 1, Min.x, Max.x)};
|
|
auto const zs{index_range(low.y, step, samples + 1, Min.y, Max.y)};
|
|
if (xs.first > xs.second || zs.first > zs.second)
|
|
continue;
|
|
if (current.paint <= 0 && current.layers[0] == Layer)
|
|
continue; // the chunk is all of this material already
|
|
auto data{std::make_shared<heightmap::chunk_data>(current)};
|
|
data->ensure_paint(samples);
|
|
auto slot{data->slot_of(Layer)};
|
|
if (slot < 0)
|
|
{
|
|
data->compact_layers();
|
|
data->ensure_paint(samples);
|
|
auto const count{data->layer_count()};
|
|
if (count >= heightmap::max_chunk_layers)
|
|
{
|
|
WriteLog("Editor: chunk " + std::to_string(current.key.first) + "," + std::to_string(current.key.second) + " has " + std::to_string(count) + " materials already, no room for another",
|
|
logtype::generic);
|
|
continue;
|
|
}
|
|
slot = static_cast<int>(count);
|
|
data->layers[slot] = Layer;
|
|
data->weights.resize(data->weights.size() + static_cast<std::size_t>(samples + 1) * (samples + 1), 0);
|
|
}
|
|
auto const plane{static_cast<std::size_t>(samples + 1) * (samples + 1)};
|
|
auto const count{data->layer_count()};
|
|
bool painted{false};
|
|
for (int iz = zs.first; iz <= zs.second; ++iz)
|
|
for (int ix = xs.first; ix <= xs.second; ++ix)
|
|
{
|
|
auto const amount{std::clamp(Amount(low.x + ix * step, low.y + iz * step), 0.0, 1.0)};
|
|
if (amount <= 0.0)
|
|
continue;
|
|
auto const sample{static_cast<std::size_t>(iz) * (samples + 1) + ix};
|
|
auto &own{data->weights[slot * plane + sample]};
|
|
auto const target{static_cast<int>(std::lround(own + (255 - own) * amount))};
|
|
if (target == own)
|
|
continue;
|
|
// the other layers give way in proportion to what they have, the weights still adding up to 255
|
|
auto const others{255 - static_cast<int>(own)};
|
|
auto const left{255 - target};
|
|
int given{0};
|
|
for (std::size_t other = 0; other < count; ++other)
|
|
{
|
|
if (static_cast<int>(other) == slot)
|
|
continue;
|
|
auto &weight{data->weights[other * plane + sample]};
|
|
auto const scaled{others > 0 ? static_cast<int>(weight) * left / others : 0};
|
|
weight = static_cast<std::uint8_t>(scaled);
|
|
given += scaled;
|
|
}
|
|
own = static_cast<std::uint8_t>(255 - given);
|
|
painted = true;
|
|
}
|
|
if (false == painted)
|
|
continue;
|
|
data->compact_layers();
|
|
remember(*chunk);
|
|
chunk->replace(std::move(data), false, true);
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
std::uint16_t terrain_streamer::layer(std::string const &Material, float const Size, float const Rotation)
|
|
{
|
|
for (std::size_t i = 0; i < m_manifest.layers.size(); ++i)
|
|
if (m_manifest.layers[i].material == Material && (Size <= 0.f || m_manifest.layers[i].size == Size) && m_manifest.layers[i].rotation == Rotation)
|
|
return static_cast<std::uint16_t>(i);
|
|
remember_manifest();
|
|
m_manifest.layers.push_back({Material, std::max(0.f, Size), Rotation});
|
|
m_manifestchanged = true;
|
|
return static_cast<std::uint16_t>(m_manifest.layers.size() - 1);
|
|
}
|
|
|
|
void terrain_streamer::layer_size(std::uint16_t const Layer, float const Size)
|
|
{
|
|
if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].size == Size)
|
|
return;
|
|
remember_manifest();
|
|
m_manifest.layers[Layer].size = std::max(0.f, Size);
|
|
m_manifestchanged = true;
|
|
// the materials of the chunks using it are made again
|
|
for (auto &entry : m_chunks)
|
|
if (entry.second->data().slot_of(Layer) >= 0)
|
|
entry.second->m_paintedversion = 0;
|
|
}
|
|
|
|
void terrain_streamer::layer_rotation(std::uint16_t const Layer, float Rotation)
|
|
{
|
|
Rotation = static_cast<float>(std::remainder(static_cast<double>(Rotation), 360.0));
|
|
if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].rotation == Rotation)
|
|
return;
|
|
remember_manifest();
|
|
m_manifest.layers[Layer].rotation = Rotation;
|
|
m_manifestchanged = true;
|
|
for (auto &entry : m_chunks)
|
|
if (entry.second->data().slot_of(Layer) >= 0 || (Layer == 0 && entry.second->data().layer_count() == 0))
|
|
entry.second->m_paintedversion = 0;
|
|
}
|
|
|
|
void terrain_streamer::layer_material(std::uint16_t const Layer, std::string const &Material)
|
|
{
|
|
if (Layer >= m_manifest.layers.size() || Material.empty() || m_manifest.layers[Layer].material == Material)
|
|
return;
|
|
remember_manifest();
|
|
m_manifest.layers[Layer].material = Material;
|
|
m_manifestchanged = true;
|
|
m_palette.erase(Layer);
|
|
for (auto &entry : m_chunks)
|
|
if (entry.second->data().slot_of(Layer) >= 0 || (Layer == 0 && entry.second->data().layer_count() == 0))
|
|
entry.second->m_paintedversion = 0;
|
|
}
|
|
|
|
void terrain_streamer::compress(bool const State)
|
|
{
|
|
if (m_manifest.compress == State)
|
|
return;
|
|
m_manifest.compress = State;
|
|
m_manifestchanged = true;
|
|
}
|
|
|
|
void terrain_streamer::default_spacing(float const Spacing)
|
|
{
|
|
auto const spacing{heightmap::valid_spacing(Spacing)};
|
|
if (m_manifest.spacing == spacing)
|
|
return;
|
|
m_manifest.spacing = spacing;
|
|
m_manifestchanged = true;
|
|
}
|
|
|
|
void terrain_streamer::water(std::vector<heightmap::water_body> Water)
|
|
{
|
|
remember_manifest();
|
|
m_manifest.water = std::move(Water);
|
|
m_manifestchanged = true;
|
|
m_waterchanged = true;
|
|
}
|
|
|
|
void terrain_streamer::water_material(std::string const &Material)
|
|
{
|
|
if (Material.empty() || Material == m_manifest.water_material)
|
|
return;
|
|
remember_manifest();
|
|
m_manifest.water_material = Material;
|
|
m_manifestchanged = true;
|
|
m_waterchanged = true;
|
|
}
|
|
|
|
std::vector<std::pair<glm::dvec2, glm::dvec2>> terrain_streamer::modifier_bounds() const
|
|
{
|
|
std::vector<std::pair<glm::dvec2, glm::dvec2>> result;
|
|
result.reserve(m_manifest.modifiers.size());
|
|
for (auto const &modifier : m_manifest.modifiers)
|
|
result.push_back(modifier.bounds());
|
|
return result;
|
|
}
|
|
|
|
void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max)
|
|
{
|
|
apply_modifiers(Chunk, Min, Max, modifier_bounds());
|
|
}
|
|
|
|
void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<std::pair<glm::dvec2, glm::dvec2>> const &Bounds)
|
|
{
|
|
auto const ¤t{Chunk.data()};
|
|
auto const low{current.corner()};
|
|
auto const high{low + glm::dvec2{heightmap::chunk_size}};
|
|
std::vector<heightmap::modifier_shape> shapes;
|
|
bool reachesany{false};
|
|
for (std::size_t i = 0; i < m_manifest.modifiers.size(); ++i)
|
|
{
|
|
// the shape of a modifier far away isn't worth making
|
|
auto const &[from, to]{Bounds[i]};
|
|
if (to.x < low.x || from.x > high.x || to.y < low.y || from.y > high.y)
|
|
continue;
|
|
heightmap::modifier_shape shape{m_manifest.modifiers[i]};
|
|
if (false == shape.reaches(low, high))
|
|
continue;
|
|
reachesany = true;
|
|
if (shape.reaches(Min, Max))
|
|
shapes.push_back(std::move(shape));
|
|
}
|
|
if (false == reachesany)
|
|
{
|
|
// no modifier reaches the chunk (any more): its own heights are what's left, with the touch-ups made over the modifiers
|
|
if (false == current.base.empty())
|
|
{
|
|
remember(Chunk);
|
|
auto &data{Chunk.edit(true, false)};
|
|
data.heights = std::move(data.base);
|
|
if (data.adjust.size() == data.heights.size())
|
|
for (std::size_t index = 0; index < data.heights.size(); ++index)
|
|
data.heights[index] += data.adjust[index];
|
|
data.base.clear();
|
|
data.adjust.clear();
|
|
}
|
|
return;
|
|
}
|
|
remember(Chunk);
|
|
auto &data{Chunk.edit(true, false)};
|
|
if (data.base.empty())
|
|
{
|
|
data.base = data.heights;
|
|
data.adjust.clear();
|
|
}
|
|
auto const touchups{data.adjust.size() == data.heights.size()};
|
|
auto const step{data.spacing()};
|
|
auto const xs{index_range(low.x, step, data.side(), std::max(Min.x, low.x), std::min(Max.x, high.x))};
|
|
auto const zs{index_range(low.y, step, data.side(), std::max(Min.y, low.y), std::min(Max.y, high.y))};
|
|
for (int iz = zs.first; iz <= zs.second; ++iz)
|
|
for (int ix = xs.first; ix <= xs.second; ++ix)
|
|
{
|
|
auto const index{static_cast<std::size_t>(iz) * data.side() + ix};
|
|
auto const ground{static_cast<double>(data.base[index])};
|
|
auto const x{low.x + ix * step}, z{low.y + iz * step};
|
|
// the same for any order of the modifiers: within the formation of a track its formation, of the nearest one where two
|
|
// overlap; elsewhere the slopes, the highest fill and the lowest cut, the nearer of the two where both reach
|
|
std::optional<heightmap::modifier_shape::effect> bed, fill, cut;
|
|
for (auto const &shape : shapes)
|
|
{
|
|
auto const effect{shape.evaluate(x, z, ground)};
|
|
if (false == effect.has_value())
|
|
continue;
|
|
if (effect->outside <= 0.0)
|
|
{
|
|
if (false == bed.has_value() || effect->distance < bed->distance)
|
|
bed = effect;
|
|
}
|
|
else if (effect->height > ground)
|
|
{
|
|
if (false == fill.has_value() || effect->height > fill->height)
|
|
fill = effect;
|
|
}
|
|
else if (effect->height < ground && (false == cut.has_value() || effect->height < cut->height))
|
|
cut = effect;
|
|
}
|
|
auto height{ground};
|
|
if (bed.has_value())
|
|
height = bed->height;
|
|
else if (fill.has_value() && cut.has_value())
|
|
height = fill->outside <= cut->outside ? fill->height : cut->height;
|
|
else if (fill.has_value())
|
|
height = fill->height;
|
|
else if (cut.has_value())
|
|
height = cut->height;
|
|
data.heights[index] = static_cast<float>(touchups ? height + data.adjust[index] : height);
|
|
}
|
|
}
|
|
|
|
void terrain_streamer::apply_modifiers(glm::dvec2 const &Min, glm::dvec2 const &Max)
|
|
{
|
|
auto const bounds{modifier_bounds()};
|
|
for (auto *chunk : chunks_in(Min, Max, true))
|
|
apply_modifiers(*chunk, Min, Max, bounds);
|
|
}
|
|
|
|
void terrain_streamer::modifiers(std::vector<heightmap::modifier> Modifiers)
|
|
{
|
|
if (Modifiers.empty())
|
|
return;
|
|
remember_manifest();
|
|
std::vector<std::pair<glm::dvec2, glm::dvec2>> areas;
|
|
for (auto &modifier : Modifiers)
|
|
{
|
|
auto bounds{modifier.bounds()};
|
|
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == modifier.name; })};
|
|
if (existing != m_manifest.modifiers.end())
|
|
{
|
|
auto const old{existing->bounds()};
|
|
bounds.first = glm::min(bounds.first, old.first);
|
|
bounds.second = glm::max(bounds.second, old.second);
|
|
*existing = std::move(modifier);
|
|
}
|
|
else
|
|
m_manifest.modifiers.push_back(std::move(modifier));
|
|
areas.push_back(bounds);
|
|
}
|
|
m_manifestchanged = true;
|
|
// every chunk reached, once, whole
|
|
std::vector<editor_terrain *> chunks;
|
|
for (auto const &[from, to] : areas)
|
|
for (auto *chunk : chunks_in(from, to, true))
|
|
chunks.push_back(chunk);
|
|
std::sort(chunks.begin(), chunks.end());
|
|
chunks.erase(std::unique(chunks.begin(), chunks.end()), chunks.end());
|
|
auto const bounds{modifier_bounds()};
|
|
for (auto *chunk : chunks)
|
|
{
|
|
auto const low{chunk->data().corner()};
|
|
apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size}, bounds);
|
|
}
|
|
}
|
|
|
|
void terrain_streamer::modifier(heightmap::modifier Modifier)
|
|
{
|
|
remember_manifest();
|
|
auto bounds{Modifier.bounds()};
|
|
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Modifier.name; })};
|
|
if (existing != m_manifest.modifiers.end())
|
|
{
|
|
// the ground it changed before is led anew as well
|
|
auto const old{existing->bounds()};
|
|
bounds.first = glm::min(bounds.first, old.first);
|
|
bounds.second = glm::max(bounds.second, old.second);
|
|
*existing = std::move(Modifier);
|
|
}
|
|
else
|
|
m_manifest.modifiers.push_back(std::move(Modifier));
|
|
m_manifestchanged = true;
|
|
apply_modifiers(bounds.first, bounds.second);
|
|
}
|
|
|
|
std::size_t terrain_streamer::drop_modifiers(std::string const &Prefix)
|
|
{
|
|
auto &list{m_manifest.modifiers};
|
|
auto const matches = [&](heightmap::modifier const &Item) { return Item.name.rfind(Prefix, 0) == 0; };
|
|
auto const count{static_cast<std::size_t>(std::count_if(list.begin(), list.end(), matches))};
|
|
if (count == 0)
|
|
return 0;
|
|
remember_manifest();
|
|
list.erase(std::remove_if(list.begin(), list.end(), matches), list.end());
|
|
m_manifestchanged = true;
|
|
return count;
|
|
}
|
|
|
|
bool terrain_streamer::remove_modifier(std::string const &Name)
|
|
{
|
|
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })};
|
|
if (existing == m_manifest.modifiers.end())
|
|
return false;
|
|
remember_manifest();
|
|
auto const bounds{existing->bounds()};
|
|
m_manifest.modifiers.erase(existing);
|
|
m_manifestchanged = true;
|
|
apply_modifiers(bounds.first, bounds.second);
|
|
return true;
|
|
}
|
|
|
|
bool terrain_streamer::bake_modifier(std::string const &Name)
|
|
{
|
|
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })};
|
|
if (existing == m_manifest.modifiers.end())
|
|
return false;
|
|
remember_manifest();
|
|
auto const baked{*existing};
|
|
auto const bounds{baked.bounds()};
|
|
heightmap::modifier_shape const shape{baked};
|
|
// the ground under the modifiers takes the shape this one gives it; the other modifiers and the touch-ups go on top
|
|
for (auto *chunk : chunks_in(bounds.first, bounds.second, true))
|
|
{
|
|
auto const ¤t{chunk->data()};
|
|
if (current.base.empty())
|
|
continue;
|
|
auto const low{current.corner()};
|
|
auto const step{current.spacing()};
|
|
auto const xs{index_range(low.x, step, current.side(), bounds.first.x, bounds.second.x)};
|
|
auto const zs{index_range(low.y, step, current.side(), bounds.first.y, bounds.second.y)};
|
|
auto base{current.base};
|
|
bool shaped{false};
|
|
for (int iz = zs.first; iz <= zs.second; ++iz)
|
|
for (int ix = xs.first; ix <= xs.second; ++ix)
|
|
shaped |= shape.apply(low.x + ix * step, low.y + iz * step, base[static_cast<std::size_t>(iz) * current.side() + ix]);
|
|
if (false == shaped)
|
|
continue;
|
|
remember(*chunk);
|
|
chunk->edit(true, false).base = std::move(base);
|
|
}
|
|
m_manifest.modifiers.erase(std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; }));
|
|
m_manifestchanged = true;
|
|
apply_modifiers(bounds.first, bounds.second);
|
|
return true;
|
|
}
|
|
|
|
bool terrain_streamer::modified() const
|
|
{
|
|
if (m_manifestchanged || false == m_removed.empty())
|
|
return true;
|
|
return std::any_of(m_chunks.begin(), m_chunks.end(), [](auto const &Entry) { return Entry.second->modified(); });
|
|
}
|
|
|
|
// undo
|
|
|
|
void terrain_streamer::begin_change()
|
|
{
|
|
if (m_recording++ == 0)
|
|
m_change = change();
|
|
}
|
|
|
|
terrain_streamer::change terrain_streamer::end_change()
|
|
{
|
|
if (m_recording == 0)
|
|
return change();
|
|
if (--m_recording > 0)
|
|
return change(); // the outer change goes on
|
|
change result{std::move(m_change)};
|
|
m_change = change();
|
|
return result;
|
|
}
|
|
|
|
void terrain_streamer::remember(chunk_key const &Key, heightmap::chunk_ptr const &Data)
|
|
{
|
|
if (m_recording > 0)
|
|
m_change.chunks.emplace(Key, Data); // only the first state counts
|
|
}
|
|
|
|
void terrain_streamer::remember_manifest()
|
|
{
|
|
if (m_recording == 0 || m_change.manifest)
|
|
return;
|
|
m_change.manifest = true;
|
|
m_change.layers = m_manifest.layers;
|
|
m_change.water = m_manifest.water;
|
|
m_change.water_material = m_manifest.water_material;
|
|
m_change.modifiers = m_manifest.modifiers;
|
|
}
|
|
|
|
terrain_streamer::change terrain_streamer::restore(change const &Change)
|
|
{
|
|
change opposite;
|
|
if (false == m_active)
|
|
return opposite;
|
|
for (auto const &[key, data] : Change.chunks)
|
|
{
|
|
auto *current{load_now(key)};
|
|
opposite.chunks.emplace(key, current != nullptr ? current->shared() : nullptr);
|
|
if (data == nullptr)
|
|
{
|
|
if (current != nullptr)
|
|
{
|
|
undraw(*current);
|
|
m_chunks.erase(key);
|
|
m_removed.insert(key);
|
|
touch_neighbours(key);
|
|
}
|
|
continue;
|
|
}
|
|
if (current != nullptr)
|
|
{
|
|
current->replace(data, true, true);
|
|
}
|
|
else
|
|
{
|
|
auto chunk{std::make_unique<editor_terrain>(data)};
|
|
chunk->m_modified = true;
|
|
auto const [low, high]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())};
|
|
auto const corner{data->corner()};
|
|
chunk->m_low = {corner.x, *low, corner.y};
|
|
chunk->m_high = {corner.x + heightmap::chunk_size, *high, corner.y + heightmap::chunk_size};
|
|
m_chunks.emplace(key, std::move(chunk));
|
|
m_removed.erase(key);
|
|
m_loading.erase(key);
|
|
}
|
|
touch_neighbours(key);
|
|
}
|
|
if (Change.manifest)
|
|
{
|
|
opposite.manifest = true;
|
|
opposite.layers = m_manifest.layers;
|
|
opposite.water = m_manifest.water;
|
|
opposite.water_material = m_manifest.water_material;
|
|
opposite.modifiers = m_manifest.modifiers;
|
|
auto const samelayers{std::equal(Change.layers.begin(), Change.layers.end(), m_manifest.layers.begin(), m_manifest.layers.end(),
|
|
[](heightmap::layer_def const &A, heightmap::layer_def const &B) { return A.material == B.material && A.size == B.size && A.rotation == B.rotation; })};
|
|
m_manifest.layers = Change.layers;
|
|
m_manifest.modifiers = Change.modifiers;
|
|
m_manifest.water = Change.water;
|
|
if (false == Change.water_material.empty())
|
|
m_manifest.water_material = Change.water_material;
|
|
m_manifestchanged = true;
|
|
m_waterchanged = true;
|
|
if (false == samelayers)
|
|
{
|
|
// the materials of the chunks are made anew from the palette as it was
|
|
m_palette.clear();
|
|
for (auto &entry : m_chunks)
|
|
entry.second->m_paintedversion = 0;
|
|
}
|
|
}
|
|
return opposite;
|
|
}
|
|
|
|
bool terrain_streamer::save(std::string *Error)
|
|
{
|
|
if (false == m_active)
|
|
return true;
|
|
auto const fail = [&](std::string const &Message) {
|
|
if (Error != nullptr)
|
|
*Error = Message;
|
|
ErrorLog("Heightmap terrain: " + Message);
|
|
return false;
|
|
};
|
|
std::error_code ec;
|
|
auto const manifestpath{m_directory + "terrain.yaml"};
|
|
if (m_manifestchanged || modified() || false == std::filesystem::exists(manifestpath, ec))
|
|
{
|
|
std::string error;
|
|
if (false == heightmap::save_manifest(manifestpath, m_manifest, &error))
|
|
return fail(error);
|
|
m_manifestchanged = false;
|
|
}
|
|
|
|
// the packs holding changed chunks are written whole
|
|
std::set<heightmap::pack_key> packs;
|
|
for (auto const &entry : m_chunks)
|
|
if (entry.second->modified())
|
|
packs.insert(heightmap::pack_of(entry.first));
|
|
for (auto const &key : m_removed)
|
|
packs.insert(heightmap::pack_of(key));
|
|
|
|
for (auto const &packkey : packs)
|
|
{
|
|
auto const path{pack_path(packkey)};
|
|
std::vector<heightmap::chunk_ptr> chunks;
|
|
std::set<chunk_key> taken;
|
|
// what's in the file now, unless it's in memory or removed
|
|
std::shared_ptr<heightmap::pack_file> file;
|
|
if (std::filesystem::exists(path, ec))
|
|
{
|
|
file = std::make_shared<heightmap::pack_file>();
|
|
std::string error;
|
|
std::lock_guard<std::mutex> lock{m_filemutex};
|
|
if (false == file->open(path, &error))
|
|
return fail(error);
|
|
for (auto const &key : file->chunks())
|
|
{
|
|
if (m_removed.count(key) > 0 || m_chunks.count(key) > 0)
|
|
continue;
|
|
auto data{file->read(key, &error)};
|
|
if (data == nullptr)
|
|
return fail(error);
|
|
chunks.push_back(std::move(data));
|
|
taken.insert(key);
|
|
}
|
|
}
|
|
for (auto const &entry : m_chunks)
|
|
if (heightmap::pack_of(entry.first) == packkey && taken.count(entry.first) == 0)
|
|
chunks.push_back(entry.second->shared());
|
|
{
|
|
std::lock_guard<std::mutex> lock{m_filemutex};
|
|
if (chunks.empty())
|
|
std::filesystem::remove(path, ec);
|
|
else
|
|
{
|
|
std::string error;
|
|
if (false == heightmap::write_pack(path, packkey, chunks, m_manifest.compress, &error))
|
|
return fail(error);
|
|
}
|
|
}
|
|
// the file is known anew; what the workers read from the old one is ignored
|
|
auto &pack{m_packs[packkey]};
|
|
++pack.generation;
|
|
pack.file.reset();
|
|
pack.status = pack_state::state::missing;
|
|
if (false == chunks.empty())
|
|
{
|
|
auto reopened{std::make_shared<heightmap::pack_file>()};
|
|
std::string error;
|
|
std::lock_guard<std::mutex> lock{m_filemutex};
|
|
if (reopened->open(path, &error))
|
|
{
|
|
pack.file = reopened;
|
|
pack.status = pack_state::state::ready;
|
|
}
|
|
}
|
|
for (auto it = m_removed.begin(); it != m_removed.end();)
|
|
it = (heightmap::pack_of(*it) == packkey ? m_removed.erase(it) : std::next(it));
|
|
for (auto const &entry : m_chunks)
|
|
if (heightmap::pack_of(entry.first) == packkey)
|
|
entry.second->clear_modified();
|
|
}
|
|
WriteLog("Heightmap terrain saved: " + m_directory + " (" + std::to_string(packs.size()) + " pack files written)", logtype::generic);
|
|
return true;
|
|
}
|
|
|
|
// water
|
|
|
|
void terrain_streamer::clear_water(bool const Touch)
|
|
{
|
|
if (Touch && simulation::Region != nullptr)
|
|
for (auto const &water : m_watershapes)
|
|
{
|
|
if (water.cell != nullptr)
|
|
erase_shape(water.cell->m_shapestranslucent, water.geometry);
|
|
else if (water.section != nullptr)
|
|
erase_shape(water.section->m_shapes, water.geometry);
|
|
}
|
|
// the geometry stays with the renderer, to be filled anew
|
|
for (auto const &water : m_watershapes)
|
|
m_waterfree.push_back(water.geometry);
|
|
m_watershapes.clear();
|
|
}
|
|
|
|
void terrain_streamer::build_water()
|
|
{
|
|
m_waterchanged = false;
|
|
clear_water(true);
|
|
if (simulation::Region == nullptr)
|
|
return;
|
|
for (auto const &body : m_manifest.water)
|
|
{
|
|
if (body.outline.size() < 3)
|
|
continue;
|
|
auto const indices{heightmap::triangulate(body.outline)};
|
|
if (indices.empty())
|
|
continue;
|
|
glm::dvec2 centroid{0.0};
|
|
for (auto const &point : body.outline)
|
|
centroid += point;
|
|
centroid /= static_cast<double>(body.outline.size());
|
|
if (false == in_region(centroid))
|
|
continue;
|
|
auto const material{GfxRenderer->Fetch_Material(body.material.empty() ? m_manifest.water_material : body.material)};
|
|
auto const *materialdata{GfxRenderer->Material(material)};
|
|
auto const repeat{body.size > 0.f ? static_cast<double>(body.size) : (materialdata != nullptr && materialdata->GetSize().x > 0.f) ? static_cast<double>(materialdata->GetSize().x) : 8.0};
|
|
glm::dvec3 const centre{centroid.x, body.level, centroid.y};
|
|
auto §ion{simulation::Region->section(centre)};
|
|
// the section makes the geometry of its own shapes once, before it's drawn; done first, it leaves ours alone
|
|
section.create_geometry();
|
|
// translucent water goes with the translucent geometry of the cell around its middle, drawn relative to the cell
|
|
auto const translucent{materialdata != nullptr && materialdata->is_translucent()};
|
|
auto &cell{section.cell(centre)};
|
|
auto const origin{translucent ? cell.m_area.center : section.m_area.center};
|
|
gfx::vertex_array vertices;
|
|
vertices.reserve(indices.size());
|
|
double reach{0.0};
|
|
// the outline is turned counterclockwise in the plan, the triangles go the other way round to face up
|
|
for (std::size_t i = 0; i + 2 < indices.size(); i += 3)
|
|
for (auto const index : {indices[i + 2], indices[i + 1], indices[i]})
|
|
{
|
|
auto const &point{body.outline[index]};
|
|
gfx::basic_vertex vertex;
|
|
vertex.position = glm::vec3(glm::dvec3{point.x, body.level, point.y} - origin);
|
|
vertex.normal = {0.f, 1.f, 0.f};
|
|
vertex.texture = {static_cast<float>((point.x - centroid.x) / repeat), static_cast<float>((centroid.y - point.y) / repeat)};
|
|
vertex.tangent = {1.f, 0.f, 0.f, 1.f};
|
|
vertices.push_back(vertex);
|
|
reach = std::max(reach, glm::length(point - centroid));
|
|
}
|
|
// the geometry of water made before is filled anew, so editing the water doesn't pile up geometry
|
|
gfx::userdata_array nouserdata;
|
|
water_shape entry;
|
|
if (false == m_waterfree.empty())
|
|
{
|
|
entry.geometry = m_waterfree.back();
|
|
m_waterfree.pop_back();
|
|
GfxRenderer->Replace(vertices, nouserdata, entry.geometry, GL_TRIANGLES);
|
|
}
|
|
else
|
|
{
|
|
if (m_waterbank.bank == 0 && m_waterbank.chunk == 0)
|
|
m_waterbank = GfxRenderer->Create_Bank();
|
|
entry.geometry = GfxRenderer->Insert(vertices, nouserdata, m_waterbank, GL_TRIANGLES);
|
|
}
|
|
if (entry.geometry.chunk == 0)
|
|
continue;
|
|
// two bounding vertices give the shape its area, the triangles are in the geometry
|
|
std::vector<world_vertex> bounds(2);
|
|
bounds[0].position = glm::dvec3{centroid.x - reach, body.level, centroid.y - reach};
|
|
bounds[1].position = glm::dvec3{centroid.x + reach, body.level, centroid.y + reach};
|
|
scene::shape_node shape;
|
|
shape.make_terrain(material, std::move(bounds), origin);
|
|
shape.geometry(entry.geometry);
|
|
if (translucent)
|
|
{
|
|
cell.m_shapestranslucent.emplace_back(std::move(shape));
|
|
cell.m_area.radius = std::max(cell.m_area.radius, static_cast<float>(glm::length(cell.m_area.center - centre) + reach));
|
|
cell.m_active = true;
|
|
entry.cell = &cell;
|
|
}
|
|
else
|
|
{
|
|
section.m_shapes.emplace_back(std::move(shape));
|
|
}
|
|
section.m_area.radius = std::max(section.m_area.radius, static_cast<float>(glm::length(section.m_area.center - centre) + reach));
|
|
entry.section = §ion;
|
|
m_watershapes.push_back(entry);
|
|
}
|
|
}
|