mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-22 19:49:19 +02:00
World Streaming Part 1
This commit is contained in:
@@ -237,6 +237,7 @@ void editor_terrain::optimize(float ErrorMetres)
|
||||
{
|
||||
m_simplify = true;
|
||||
m_simplify_error = (ErrorMetres > 0.0f ? ErrorMetres : 0.01f);
|
||||
m_dirty = false;
|
||||
regenerate(true);
|
||||
}
|
||||
|
||||
@@ -246,6 +247,28 @@ void editor_terrain::unoptimize()
|
||||
regenerate(false);
|
||||
}
|
||||
|
||||
void editor_terrain::destroy()
|
||||
{
|
||||
if (m_section != nullptr)
|
||||
{
|
||||
// erase the shape whose geometry handle is ours; other shapes keep their handles (so they
|
||||
// keep rendering), and the geometry GC reclaims our now-undrawn chunk's GPU memory
|
||||
for (auto it = m_section->m_shapes.begin(); it != m_section->m_shapes.end(); ++it)
|
||||
{
|
||||
auto const h = it->data().geometry;
|
||||
if (h.bank == m_geometry.bank && h.chunk == m_geometry.chunk)
|
||||
{
|
||||
m_section->m_shapes.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_section = nullptr;
|
||||
m_geometry = gfx::geometry_handle{0, 0};
|
||||
m_cells = 0; // mark invalid
|
||||
m_heights.clear();
|
||||
}
|
||||
|
||||
bool editor_terrain::contains(double X, double Z) const
|
||||
{
|
||||
double const x1 = m_x0 + static_cast<double>(m_cells) * m_cellsize;
|
||||
@@ -298,8 +321,10 @@ bool editor_terrain::sculpt(double X, double Z, double Radius, double Strength)
|
||||
if (changed)
|
||||
{
|
||||
// sculpting edits the full-resolution mesh (fixed vertex count => fast in-place update);
|
||||
// the user can re-run optimize() afterwards to simplify again
|
||||
// mark dirty so it can be auto-simplified once the stroke finishes, and modified for saving
|
||||
m_simplify = false;
|
||||
m_dirty = true;
|
||||
m_modified = true;
|
||||
regenerate(false);
|
||||
}
|
||||
return changed;
|
||||
|
||||
@@ -55,11 +55,25 @@ class editor_terrain
|
||||
// rebuilds the rendered mesh at full resolution (undoes optimize)
|
||||
void unoptimize();
|
||||
|
||||
// removes the rendered shape from its section (stops drawing it); the renderer's geometry
|
||||
// garbage collector reclaims the GPU memory shortly after. used by terrain streaming to unload.
|
||||
void destroy();
|
||||
|
||||
// horizontal centre and extent, handy for the UI / camera framing
|
||||
glm::dvec3 centre() const;
|
||||
float extent() const { return m_cells * m_cellsize; }
|
||||
bool valid() const { return m_cells > 0; }
|
||||
bool optimized() const { return m_simplify; }
|
||||
// set by sculpt() when the mesh changed and is currently full-resolution; cleared by optimize().
|
||||
// used to auto-simplify only the chunks that were actually edited, once a stroke finishes.
|
||||
bool dirty() const { return m_dirty; }
|
||||
// set by sculpt() and never auto-cleared; used by streaming to know a chunk needs saving to disk
|
||||
bool modified() const { return m_modified; }
|
||||
void clear_modified() { m_modified = false; }
|
||||
|
||||
// direct access to the editable heightmap (row-major world Y, (cells+1)^2), for save/load
|
||||
std::vector<float> const &heights() const { return m_heights; }
|
||||
int cells() const { return m_cells; }
|
||||
// rendered triangle count (drops after optimize)
|
||||
std::size_t triangles() const { return m_vertexcount / 3; }
|
||||
// full-resolution triangle count, for reference
|
||||
@@ -94,4 +108,6 @@ class editor_terrain
|
||||
|
||||
bool m_simplify{false}; // whether the rendered mesh is currently simplified
|
||||
float m_simplify_error{0.5f}; // flatness tolerance used by optimize()
|
||||
bool m_dirty{false}; // edited since the last optimize (full-res, awaiting simplification)
|
||||
bool m_modified{false}; // edited since load/save (for streaming persistence)
|
||||
};
|
||||
|
||||
289
editor/editorTerrainStreamer.cpp
Normal file
289
editor/editorTerrainStreamer.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 "editor/editorTerrainStreamer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
// simulation-level streamer instance (see header)
|
||||
terrain_streamer EditorTerrain;
|
||||
|
||||
void terrain_streamer::configure(int Cells, float CellSize, int Radius, float BaseHeight, std::string const &Texture)
|
||||
{
|
||||
m_cells = std::max(1, Cells);
|
||||
m_cellsize = std::max(0.1f, CellSize);
|
||||
m_radius = std::max(0, Radius);
|
||||
m_baseheight = BaseHeight;
|
||||
m_texture = Texture;
|
||||
}
|
||||
|
||||
terrain_streamer::chunk_key terrain_streamer::key_at(double X, double Z) const
|
||||
{
|
||||
double const size = chunk_world_size();
|
||||
return {static_cast<int>(std::floor(X / size)), static_cast<int>(std::floor(Z / size))};
|
||||
}
|
||||
|
||||
glm::dvec3 terrain_streamer::chunk_centre(int Cx, int Cz) const
|
||||
{
|
||||
double const size = chunk_world_size();
|
||||
return glm::dvec3((Cx + 0.5) * size, static_cast<double>(m_baseheight), (Cz + 0.5) * size);
|
||||
}
|
||||
|
||||
std::string terrain_streamer::chunk_path(int Cx, int Cz) const
|
||||
{
|
||||
return m_dir + "/chunk_" + std::to_string(Cx) + "_" + std::to_string(Cz) + ".etc";
|
||||
}
|
||||
|
||||
bool terrain_streamer::chunk_on_disk(chunk_key const &Key)
|
||||
{
|
||||
auto const it = m_known.find(Key);
|
||||
if (it != m_known.end())
|
||||
return (it->second & cf_exists_on_disk) != 0;
|
||||
|
||||
std::error_code ec;
|
||||
bool const exists = std::filesystem::exists(chunk_path(Key.first, Key.second), ec);
|
||||
m_known[Key] = exists ? cf_exists_on_disk : 0;
|
||||
return exists;
|
||||
}
|
||||
|
||||
// 16-bit chunk file: 'ETC1' | uint16 cells | float baseY | float step | (cells+1)^2 * uint16
|
||||
// where worldY = baseY + raw * step (per-chunk auto-scaled to fit the height range losslessly-ish)
|
||||
bool terrain_streamer::load_heights(int Cx, int Cz, std::vector<float> &Out) const
|
||||
{
|
||||
std::ifstream f(chunk_path(Cx, Cz), std::ios::binary);
|
||||
if (!f)
|
||||
return false;
|
||||
|
||||
char magic[4] = {};
|
||||
f.read(magic, 4);
|
||||
if (f.gcount() != 4 || magic[0] != 'E' || magic[1] != 'T' || magic[2] != 'C' || magic[3] != '1')
|
||||
return false;
|
||||
|
||||
std::uint16_t cells = 0;
|
||||
float baseY = 0.0f, step = 0.0f;
|
||||
f.read(reinterpret_cast<char *>(&cells), sizeof(cells));
|
||||
f.read(reinterpret_cast<char *>(&baseY), sizeof(baseY));
|
||||
f.read(reinterpret_cast<char *>(&step), sizeof(step));
|
||||
if (!f || static_cast<int>(cells) != m_cells)
|
||||
return false; // resolution changed since save: ignore and regenerate
|
||||
|
||||
std::size_t const n = static_cast<std::size_t>(cells + 1) * (cells + 1);
|
||||
Out.resize(n);
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
std::uint16_t raw = 0;
|
||||
f.read(reinterpret_cast<char *>(&raw), sizeof(raw));
|
||||
if (!f)
|
||||
return false;
|
||||
Out[i] = baseY + static_cast<float>(raw) * step;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void terrain_streamer::save_heights(int Cx, int Cz, editor_terrain const &Terrain)
|
||||
{
|
||||
auto const &h = Terrain.heights();
|
||||
if (h.empty())
|
||||
return;
|
||||
|
||||
float minh = h[0], maxh = h[0];
|
||||
for (float const v : h)
|
||||
{
|
||||
minh = std::min(minh, v);
|
||||
maxh = std::max(maxh, v);
|
||||
}
|
||||
float const step = (maxh > minh) ? (maxh - minh) / 65535.0f : 0.0f;
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(m_dir, ec);
|
||||
std::ofstream f(chunk_path(Cx, Cz), std::ios::binary | std::ios::trunc);
|
||||
if (!f)
|
||||
return;
|
||||
|
||||
char const magic[4] = {'E', 'T', 'C', '1'};
|
||||
std::uint16_t const cells = static_cast<std::uint16_t>(Terrain.cells());
|
||||
f.write(magic, 4);
|
||||
f.write(reinterpret_cast<char const *>(&cells), sizeof(cells));
|
||||
f.write(reinterpret_cast<char const *>(&minh), sizeof(minh));
|
||||
f.write(reinterpret_cast<char const *>(&step), sizeof(step));
|
||||
for (float const v : h)
|
||||
{
|
||||
std::uint16_t const raw = (step > 0.0f)
|
||||
? static_cast<std::uint16_t>(std::lround((v - minh) / step))
|
||||
: std::uint16_t{0};
|
||||
f.write(reinterpret_cast<char const *>(&raw), sizeof(raw));
|
||||
}
|
||||
}
|
||||
|
||||
void terrain_streamer::update(glm::dvec3 const &CameraPos)
|
||||
{
|
||||
if (!m_active)
|
||||
return;
|
||||
|
||||
chunk_key const camera = key_at(CameraPos.x, CameraPos.z);
|
||||
|
||||
// load any missing chunk inside the radius (one build per frame keeps the hitch small)
|
||||
bool built = false;
|
||||
for (int dz = -m_radius; dz <= m_radius && !built; ++dz)
|
||||
for (int dx = -m_radius; dx <= m_radius && !built; ++dx)
|
||||
{
|
||||
chunk_key const key{camera.first + dx, camera.second + dz};
|
||||
if (m_chunks.count(key))
|
||||
continue;
|
||||
|
||||
// only stream chunks that were actually authored (exist on disk); empty space stays empty
|
||||
std::vector<float> loaded;
|
||||
if (!chunk_on_disk(key) || !load_heights(key.first, key.second, loaded))
|
||||
continue;
|
||||
|
||||
glm::dvec3 const centre = chunk_centre(key.first, key.second);
|
||||
double const size = chunk_world_size();
|
||||
double const x0 = key.first * size, z0 = key.second * size; // chunk corner
|
||||
int const cells = m_cells;
|
||||
float const cs = m_cellsize;
|
||||
editor_terrain::height_sampler sampler =
|
||||
[&loaded, x0, z0, cells, cs](double X, double Z, double &OutY) -> bool {
|
||||
int ix = static_cast<int>(std::lround((X - x0) / cs));
|
||||
int iz = static_cast<int>(std::lround((Z - z0) / cs));
|
||||
ix = std::clamp(ix, 0, cells);
|
||||
iz = std::clamp(iz, 0, cells);
|
||||
OutY = loaded[static_cast<std::size_t>(iz) * (cells + 1) + ix];
|
||||
return true;
|
||||
};
|
||||
|
||||
auto terrain = std::make_unique<editor_terrain>();
|
||||
if (terrain->create(centre, m_cells, m_cellsize, m_texture, sampler))
|
||||
{
|
||||
if (m_auto_optimize)
|
||||
terrain->optimize(m_simplify_error);
|
||||
m_chunks.emplace(key, std::move(terrain));
|
||||
built = true; // amortise: at most one new chunk per frame
|
||||
}
|
||||
}
|
||||
|
||||
// unload chunks that drifted outside the radius
|
||||
for (auto it = m_chunks.begin(); it != m_chunks.end();)
|
||||
{
|
||||
int const dx = it->first.first - camera.first;
|
||||
int const dz = it->first.second - camera.second;
|
||||
if (std::abs(dx) > m_radius || std::abs(dz) > m_radius)
|
||||
{
|
||||
if (it->second)
|
||||
{
|
||||
// persist edits before dropping the chunk, so they survive the round-trip
|
||||
if (m_persist && it->second->modified())
|
||||
{
|
||||
save_heights(it->first.first, it->first.second, *it->second);
|
||||
m_known[it->first] |= cf_exists_on_disk;
|
||||
}
|
||||
it->second->destroy();
|
||||
}
|
||||
it = m_chunks.erase(it);
|
||||
}
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
void terrain_streamer::clear()
|
||||
{
|
||||
for (auto &entry : m_chunks)
|
||||
if (entry.second)
|
||||
{
|
||||
if (m_persist && entry.second->modified())
|
||||
{
|
||||
save_heights(entry.first.first, entry.first.second, *entry.second);
|
||||
m_known[entry.first] |= cf_exists_on_disk;
|
||||
}
|
||||
entry.second->destroy();
|
||||
}
|
||||
m_chunks.clear();
|
||||
}
|
||||
|
||||
void terrain_streamer::collect(std::vector<editor_terrain *> &Out) const
|
||||
{
|
||||
for (auto const &entry : m_chunks)
|
||||
if (entry.second)
|
||||
Out.push_back(entry.second.get());
|
||||
}
|
||||
|
||||
editor_terrain *terrain_streamer::terrain_at(double X, double Z) const
|
||||
{
|
||||
auto const it = m_chunks.find(key_at(X, Z));
|
||||
if (it != m_chunks.end() && it->second && it->second->contains(X, Z))
|
||||
return it->second.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void terrain_streamer::save_chunk(int Cx, int Cz, editor_terrain const &Terrain)
|
||||
{
|
||||
save_heights(Cx, Cz, Terrain);
|
||||
m_known[{Cx, Cz}] |= cf_exists_on_disk;
|
||||
}
|
||||
|
||||
void terrain_streamer::add_chunk(int Cx, int Cz)
|
||||
{
|
||||
chunk_key const key{Cx, Cz};
|
||||
if (m_chunks.count(key))
|
||||
return; // already resident
|
||||
|
||||
glm::dvec3 const centre = chunk_centre(Cx, Cz);
|
||||
auto terrain = std::make_unique<editor_terrain>();
|
||||
if (!terrain->create(centre, m_cells, m_cellsize, m_texture)) // flat
|
||||
return;
|
||||
|
||||
// persist immediately so it becomes part of the authored, streamable world
|
||||
save_heights(Cx, Cz, *terrain);
|
||||
m_known[key] |= cf_exists_on_disk;
|
||||
|
||||
if (m_auto_optimize)
|
||||
terrain->optimize(m_simplify_error);
|
||||
m_chunks.emplace(key, std::move(terrain));
|
||||
}
|
||||
|
||||
void terrain_streamer::reset()
|
||||
{
|
||||
// do NOT call editor_terrain::destroy() here: on a scenery reload the sections those chunks
|
||||
// referenced are already gone. just drop our bookkeeping; the old region freed the shapes.
|
||||
m_chunks.clear();
|
||||
m_known.clear();
|
||||
m_active = false;
|
||||
}
|
||||
|
||||
void terrain_streamer::flush()
|
||||
{
|
||||
for (auto &entry : m_chunks)
|
||||
if (entry.second && entry.second->modified())
|
||||
{
|
||||
save_heights(entry.first.first, entry.first.second, *entry.second);
|
||||
m_known[entry.first] |= cf_exists_on_disk;
|
||||
entry.second->clear_modified();
|
||||
}
|
||||
}
|
||||
|
||||
void terrain_streamer::remove_chunk(int Cx, int Cz)
|
||||
{
|
||||
chunk_key const key{Cx, Cz};
|
||||
auto const it = m_chunks.find(key);
|
||||
if (it != m_chunks.end())
|
||||
{
|
||||
if (it->second)
|
||||
it->second->destroy();
|
||||
m_chunks.erase(it);
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(chunk_path(Cx, Cz), ec);
|
||||
m_known[key] = 0; // no longer on disk
|
||||
}
|
||||
116
editor/editorTerrainStreamer.hpp
Normal file
116
editor/editorTerrainStreamer.hpp
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 <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "editor/editorTerrain.hpp"
|
||||
|
||||
// Streams editable terrain chunks around the camera for an effectively unbounded world.
|
||||
//
|
||||
// Phase 1: keeps a ring of resident chunks (each an editor_terrain mesh) within a radius of the
|
||||
// camera; chunks entering the radius are built, chunks leaving it are destroyed (the renderer's
|
||||
// geometry GC reclaims their GPU memory). Heights are flat + a gentle procedural roll so streaming
|
||||
// is observable. Phase 2 will add 16-bit on-disk chunk paging, per-chunk flag bits and persistence.
|
||||
class terrain_streamer
|
||||
{
|
||||
public:
|
||||
using chunk_key = std::pair<int, int>; // (cx, cz) chunk coordinate
|
||||
|
||||
terrain_streamer() = default;
|
||||
|
||||
void active(bool State) { m_active = State; }
|
||||
bool active() const { return m_active; }
|
||||
|
||||
// per-chunk configuration (applied to chunks built from now on)
|
||||
void configure(int Cells, float CellSize, int Radius, float BaseHeight, std::string const &Texture);
|
||||
void simplify(bool Auto, float Error) { m_auto_optimize = Auto; m_simplify_error = Error; }
|
||||
// safe to change while resident (only affects how far chunks load/unload)
|
||||
void radius(int Radius) { m_radius = Radius < 0 ? 0 : Radius; }
|
||||
// persist edited chunks to disk on unload, and load them back instead of regenerating
|
||||
void persist(bool Enable) { m_persist = Enable; }
|
||||
void directory(std::string const &Dir)
|
||||
{
|
||||
if (!Dir.empty() && Dir != m_dir)
|
||||
{
|
||||
m_dir = Dir;
|
||||
m_known.clear(); // existence cache is per-folder; drop it when the folder changes
|
||||
}
|
||||
}
|
||||
std::string const &directory() const { return m_dir; }
|
||||
|
||||
// loads/unloads chunks so the resident set matches the radius around CameraPos
|
||||
void update(glm::dvec3 const &CameraPos);
|
||||
// drops all resident chunks (e.g. when streaming is switched off), saving edits first
|
||||
void clear();
|
||||
// hard reset used when a new scenery loads: drops everything WITHOUT touching scene sections
|
||||
// (the old region is being torn down, so its section pointers are already dangling)
|
||||
void reset();
|
||||
|
||||
// appends raw pointers to every resident chunk (for sculpt / snap / optimize routing)
|
||||
void collect(std::vector<editor_terrain *> &Out) const;
|
||||
// resident chunk whose footprint covers (X,Z), or nullptr
|
||||
editor_terrain *terrain_at(double X, double Z) const;
|
||||
|
||||
// authoring: create a flat chunk at (Cx,Cz), make it resident and persist it to disk
|
||||
void add_chunk(int Cx, int Cz);
|
||||
// authoring: delete the chunk at (Cx,Cz) from memory and disk
|
||||
void remove_chunk(int Cx, int Cz);
|
||||
// persist an externally-authored chunk (e.g. a manual grid chunk being handed over to streaming)
|
||||
void save_chunk(int Cx, int Cz, editor_terrain const &Terrain);
|
||||
// chunk coordinate covering (X,Z)
|
||||
chunk_key key_for(double X, double Z) const { return key_at(X, Z); }
|
||||
|
||||
std::size_t resident() const { return m_chunks.size(); }
|
||||
float chunk_world_size() const { return m_cells * m_cellsize; }
|
||||
int cells() const { return m_cells; }
|
||||
float cellsize() const { return m_cellsize; }
|
||||
int radius() const { return m_radius; }
|
||||
|
||||
// writes every resident, edited chunk to disk (without unloading) - used on save
|
||||
void flush();
|
||||
|
||||
private:
|
||||
// per-chunk state bits cached so we don't stat the filesystem repeatedly
|
||||
enum chunk_flag : uint8_t
|
||||
{
|
||||
cf_exists_on_disk = 1u << 0, // a saved 16-bit chunk file exists
|
||||
};
|
||||
|
||||
chunk_key key_at(double X, double Z) const;
|
||||
glm::dvec3 chunk_centre(int Cx, int Cz) const;
|
||||
std::string chunk_path(int Cx, int Cz) const;
|
||||
bool chunk_on_disk(chunk_key const &Key); // cached existence test
|
||||
bool load_heights(int Cx, int Cz, std::vector<float> &Out) const; // reads a 16-bit chunk file
|
||||
void save_heights(int Cx, int Cz, editor_terrain const &Terrain); // writes a 16-bit chunk file
|
||||
|
||||
bool m_active{false};
|
||||
int m_cells{32};
|
||||
float m_cellsize{2.0f};
|
||||
int m_radius{2}; // chunks loaded around the camera (Chebyshev radius)
|
||||
float m_baseheight{0.0f};
|
||||
std::string m_texture;
|
||||
bool m_auto_optimize{true};
|
||||
float m_simplify_error{0.5f};
|
||||
bool m_persist{true};
|
||||
std::string m_dir{"editor_terrain"}; // folder for 16-bit chunk files
|
||||
|
||||
std::map<chunk_key, std::unique_ptr<editor_terrain>> m_chunks;
|
||||
std::map<chunk_key, std::uint8_t> m_known; // cached per-chunk flag bits
|
||||
};
|
||||
|
||||
// single, simulation-level streamer instance shared by the editor (authoring) and the scenery
|
||||
// loader (so streamed terrain renders in every mode, including the driver). loaded from the
|
||||
// `editorterrain` scenery directive and updated each frame with the active camera.
|
||||
extern terrain_streamer EditorTerrain;
|
||||
Reference in New Issue
Block a user