mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-28 12:17:29 +01:00
Cook the ground into a quadtree of mesh tiles, and keep the mesh the scenery drew
The ground in a scenery is already an irregular mesh, so the cook keeps it: it cuts the triangles along the tile grid, welds what coincides, and builds each coarser level by collapsing edges. Nothing is resampled, nothing is interpolated, and a hole the scenery left stays a hole. What was here before threw the triangles away, kept their vertices and built a fresh Delaunay triangulation over them, which filled every hole, merged the two heights of every retaining wall into one, and - because it never erased the super-triangle - emitted indices three too large together with triangles reaching out to the artificial corners. Tiles form a quadtree, as Cesium Quantized Mesh intends. A distant tile is a large one, so the horizon costs tens of tiles rather than the thousands a fixed grid needed, and the level to draw follows from the tile's own geometric error against what the screen can resolve - which is what the format carries that field for. Every vertex on a tile's border survives into all of its coarser levels, and so does every edge between two of them, so two neighbours drawn at different levels share their border exactly: there is nothing to stitch and nothing to morph. The file layout is the format's, with two deliberate departures, both written down in quantizedmeshformat.h: the geodetic header fields carry scenery metres, there being no geographic reference to convert from, and the material a vertex belongs to rides in an extension of ours. Texture coordinates are not stored at all - ground tiles in world space, so the coordinate follows from the position and from how many metres one repeat of the material covers, measured off the scenery once and kept in the terrain's table. Tiles live in one archive beside the scenery, an entry each, compressed on its own. Baltyk cooks to 546 kB against 2.2 MB of heightfields, quark to 361 kB against 9.6 MB, tomaszewo to 375 kB against 9.9 MB. Drawing is one call a frame: every resident tile lives in the same vertex and index buffer, with its vertices measured from the middle of the terrain so that the whole map fits in floats, and a frame is one glMultiDrawElementsBaseVertex over the tiles the walk chose and the frustum kept. On baltyk that settles at 291 tiles resident, 16 MB of mesh, 85 tiles and 6k triangles drawn. What counts as ground is what the scenery says, not what its triangles look like. A node under origin, rotate or scale is an object standing on the ground; a material with an alpha channel is a tree, a bush, a fence or a catenary mast. Everything else is laid whichever way it faces, so the side of an embankment or a cutting goes in with the rest instead of leaving a hole along every bank. Whatever is left out is recorded per node, and the scenery goes on drawing it. A cook is a cache: it is used again only when the archive, the note of which nodes went into it and the manifest of what the scenery was made from all agree. A manifest listing a file the scenery names but does not have used to fail that test for ever, since an absent file never matches - quark has such an include, and nothing beside it could ever be reused. tools/terraincook cooks the same way outside the engine, and meshtest beside it checks what the cook promises: a tile reads back as written, neighbouring tiles agree on their border to the millimetre, a hole is not filled in, a wall is laid exactly once, and a material's tiling comes out of the source. seamcheck.py goes: it tested the morphing constants of a heightfield nothing reads any more.
This commit is contained in:
@@ -2932,12 +2932,16 @@ void opengl33_renderer::Render_terrain()
|
||||
if (status.path.empty())
|
||||
{
|
||||
status.path = field.path();
|
||||
status.gridstep = field.gridstep();
|
||||
status.tilesize = field.tilesize();
|
||||
status.levels = field.levels();
|
||||
for (std::uint32_t level = 0; level < field.levels() && level < status.leveltile.size(); ++level)
|
||||
{
|
||||
status.leveltile[level] = field.leveltile(level);
|
||||
status.levelerror[level] = field.levelerror(level);
|
||||
}
|
||||
}
|
||||
status.range = field.range();
|
||||
status.finest = field.finest();
|
||||
status.detail = field.detailpixels();
|
||||
status.backlog = field.backlog();
|
||||
status.stats = field.stats();
|
||||
}
|
||||
|
||||
@@ -9,28 +9,27 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
module;
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "glad/glad.h"
|
||||
|
||||
#include "scene/heightfieldformat.h"
|
||||
#include "scene/heightfieldreader.h"
|
||||
#include "scene/quantizedmeshreader.h"
|
||||
|
||||
module eu07.rendering.terrainclipmap;
|
||||
import eu07.glm;
|
||||
import eu07.utilities.logs;
|
||||
import eu07.model.texture;
|
||||
|
||||
|
||||
terrain_clipmap::~terrain_clipmap() {
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -39,19 +38,34 @@ terrain_clipmap::open( std::string const &Path ) {
|
||||
|
||||
close();
|
||||
|
||||
if( ( false == m_reader.open( Path ) )
|
||||
|| ( false == m_loader.open( Path ) ) ) {
|
||||
ErrorLog( "Terrain: cannot open heightfield \"" + Path + "\"" );
|
||||
if( false == m_reader.open( Path ) ) {
|
||||
ErrorLog( "Terrain: cannot read cooked terrain \"" + Path + "\"" );
|
||||
return false;
|
||||
}
|
||||
m_table = m_reader.table();
|
||||
m_levels = std::min<std::uint32_t>( m_table.levels, terrain_maxlevels );
|
||||
m_finesttile = static_cast<float>( m_table.tilesize( m_levels - 1 ) );
|
||||
// every vertex of every tile is measured from the middle of the terrain, so that the whole of
|
||||
// it fits in floats: at ten kilometres a float still holds a millimetre
|
||||
m_origin = glm::dvec3 {
|
||||
m_table.west + m_table.side * 0.5,
|
||||
( m_table.lowest + m_table.highest ) * 0.5,
|
||||
m_table.north + m_table.side * 0.5 };
|
||||
|
||||
if( false == m_loader.open( Path, m_origin.x, m_origin.y, m_origin.z ) ) {
|
||||
ErrorLog( "Terrain: cannot open \"" + Path + "\" for reading" );
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if( false == m_arena.create() ) {
|
||||
ErrorLog( "Terrain: cannot allocate the terrain buffers" );
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_path = Path;
|
||||
build_indices();
|
||||
|
||||
try {
|
||||
gl::shader vertex( "terrainclipmap.vert" );
|
||||
gl::shader fragment( "terrainclipmap.frag" );
|
||||
gl::shader vertex( "terrainmesh.vert" );
|
||||
gl::shader fragment( "terrainmesh.frag" );
|
||||
m_shader.emplace( std::vector<std::reference_wrapper<gl::shader const>>( { vertex, fragment } ) );
|
||||
}
|
||||
catch( gl::shader_exception const &error ) {
|
||||
@@ -59,48 +73,44 @@ terrain_clipmap::open( std::string const &Path ) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if( false == m_ground.create( m_reader.materials(), m_reader.material_scales() ) ) {
|
||||
|
||||
std::vector<std::string> names;
|
||||
std::vector<float> repeats;
|
||||
names.reserve( m_table.materials.size() );
|
||||
repeats.reserve( m_table.materials.size() );
|
||||
for( auto const &material : m_table.materials ) {
|
||||
names.push_back( material.name );
|
||||
repeats.push_back( material.repeat );
|
||||
}
|
||||
if( false == m_ground.create( names, repeats ) ) {
|
||||
ErrorLog( "Terrain: ground textures failed" );
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
for( std::uint32_t level = 0; level < m_reader.levels(); ++level ) {
|
||||
m_pools.emplace_back( std::make_unique<terrain_tile_pool>( m_reader.samples_per_side( level ) ) );
|
||||
}
|
||||
m_instances.resize( m_reader.levels() );
|
||||
m_instancebuffer.emplace();
|
||||
::glGenTextures( 1, &m_instancetexture );
|
||||
opengl_texture::reset_unit_cache();
|
||||
|
||||
m_vao.emplace();
|
||||
resolve_uniforms();
|
||||
m_path = Path;
|
||||
m_ready = true;
|
||||
|
||||
WriteLog(
|
||||
"Terrain: " + Path + ", " + std::to_string( m_reader.tiles().size() ) + " tiles, "
|
||||
+ std::to_string( m_reader.levels() ) + " levels, grid "
|
||||
+ std::to_string( m_reader.gridstep() ) + " m, tile " + std::to_string( m_reader.tilesize() ) + " m" );
|
||||
|
||||
"Terrain: " + Path + ", " + std::to_string( m_levels ) + " levels, finest tile "
|
||||
+ std::to_string( static_cast<int>( m_finesttile ) ) + " m over "
|
||||
+ std::to_string( static_cast<int>( m_table.side ) ) + " m, "
|
||||
+ std::to_string( m_reader.present().size() ) + " tiles, "
|
||||
+ std::to_string( m_table.materials.size() ) + " ground materials" );
|
||||
return true;
|
||||
}
|
||||
|
||||
// uniform locations do not change once the program is linked, so they are looked up here
|
||||
// rather than per draw
|
||||
// uniform locations do not change once the program is linked, so they are looked up here rather
|
||||
// than per draw
|
||||
void
|
||||
terrain_clipmap::resolve_uniforms() {
|
||||
|
||||
auto const program { static_cast<GLuint>( *m_shader ) };
|
||||
m_uniforms.samplestep = ::glGetUniformLocation( program, "samplestep" );
|
||||
m_uniforms.side = ::glGetUniformLocation( program, "side" );
|
||||
m_uniforms.morph = ::glGetUniformLocation( program, "morph" );
|
||||
m_uniforms.heightbias = ::glGetUniformLocation( program, "heightbias" );
|
||||
m_uniforms.heightscale = ::glGetUniformLocation( program, "heightscale" );
|
||||
m_uniforms.nodata = ::glGetUniformLocation( program, "nodata" );
|
||||
m_uniforms.heights = ::glGetUniformLocation( program, "heights" );
|
||||
m_uniforms.materials = ::glGetUniformLocation( program, "materials" );
|
||||
m_uniforms.instances = ::glGetUniformLocation( program, "instances" );
|
||||
m_uniforms.origin = ::glGetUniformLocation( program, "terrainorigin" );
|
||||
m_uniforms.world = ::glGetUniformLocation( program, "terrainworld" );
|
||||
m_uniforms.ground = ::glGetUniformLocation( program, "ground" );
|
||||
m_uniforms.materialtable = ::glGetUniformLocation( program, "materialtable" );
|
||||
m_uniforms.materials = ::glGetUniformLocation( program, "materialtable" );
|
||||
}
|
||||
|
||||
void
|
||||
@@ -108,367 +118,368 @@ terrain_clipmap::close() {
|
||||
|
||||
// the loader first, so nothing arrives for tiles that are being torn down
|
||||
m_loader.close();
|
||||
m_tiles.clear();
|
||||
m_chosen.clear();
|
||||
m_wishes.clear();
|
||||
m_wanted.clear();
|
||||
m_arrived.clear();
|
||||
m_inrange.clear();
|
||||
m_wantedlevel.clear();
|
||||
m_scanrange = -1.f;
|
||||
m_tiles.clear();
|
||||
m_pools.clear();
|
||||
m_indices.clear();
|
||||
m_indexcounts.clear();
|
||||
m_instances.clear();
|
||||
m_instancebuffer.reset();
|
||||
if( m_instancetexture != 0 ) { ::glDeleteTextures( 1, &m_instancetexture ); m_instancetexture = 0; }
|
||||
m_asked.clear();
|
||||
m_readiness.clear();
|
||||
m_arena.destroy();
|
||||
m_ground.destroy();
|
||||
m_vao.reset();
|
||||
m_shader.reset();
|
||||
m_reader.close();
|
||||
m_table = {};
|
||||
m_stats = {};
|
||||
m_walked = false;
|
||||
m_walkrange = -1.f;
|
||||
m_ready = false;
|
||||
m_path.clear();
|
||||
}
|
||||
|
||||
// index buffers, one per mip level. the grid they describe is the same shape at every
|
||||
// level, only smaller, and the vertices themselves never exist in memory: the shader
|
||||
// derives a sample's row and column from gl_VertexID
|
||||
void
|
||||
terrain_clipmap::build_indices() {
|
||||
float
|
||||
terrain_clipmap::leveltile( std::uint32_t const Level ) const {
|
||||
|
||||
m_indices.resize( m_reader.levels() );
|
||||
m_indexcounts.resize( m_reader.levels() );
|
||||
|
||||
std::vector<std::uint32_t> indices;
|
||||
for( std::uint32_t level = 0; level < m_reader.levels(); ++level ) {
|
||||
|
||||
auto const side { m_reader.samples_per_side( level ) };
|
||||
indices.clear();
|
||||
indices.reserve( static_cast<std::size_t>( side - 1 ) * ( side - 1 ) * 6 );
|
||||
|
||||
for( std::uint32_t row = 0; row + 1 < side; ++row ) {
|
||||
for( std::uint32_t column = 0; column + 1 < side; ++column ) {
|
||||
auto const topleft { row * side + column };
|
||||
auto const topright { topleft + 1 };
|
||||
auto const bottomleft { topleft + side };
|
||||
auto const bottomright { bottomleft + 1 };
|
||||
indices.push_back( topleft ); indices.push_back( bottomleft ); indices.push_back( topright );
|
||||
indices.push_back( topright ); indices.push_back( bottomleft ); indices.push_back( bottomright );
|
||||
}
|
||||
}
|
||||
|
||||
m_indexcounts[ level ] = static_cast<std::uint32_t>( indices.size() );
|
||||
m_indices[ level ] = std::make_unique<gl::buffer>();
|
||||
m_indices[ level ]->allocate(
|
||||
gl::buffer::ELEMENT_ARRAY_BUFFER, indices.size() * sizeof( std::uint32_t ), GL_STATIC_DRAW );
|
||||
m_indices[ level ]->upload(
|
||||
gl::buffer::ELEMENT_ARRAY_BUFFER, indices.data(), 0, indices.size() * sizeof( std::uint32_t ) );
|
||||
}
|
||||
return static_cast<float>( m_table.tilesize( Level ) );
|
||||
}
|
||||
|
||||
float
|
||||
terrain_clipmap::levelerror( std::uint32_t const Level ) const {
|
||||
|
||||
std::uint32_t
|
||||
terrain_clipmap::level_for( double const Distance ) const {
|
||||
|
||||
if( Distance <= m_finest ) { return 0; }
|
||||
auto const steps { static_cast<std::uint32_t>( std::log2( Distance / m_finest ) ) + 1 };
|
||||
return std::min( steps, m_reader.levels() - 1 );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::upload( terrain_tile_loader::payload const &Tile ) {
|
||||
|
||||
auto const tilekey { key( Tile.tile.x, Tile.tile.z ) };
|
||||
auto const existing { m_tiles.find( tilekey ) };
|
||||
if( existing != m_tiles.end() ) {
|
||||
// a level change moves the tile to another pool
|
||||
retire( existing->second );
|
||||
m_tiles.erase( existing );
|
||||
}
|
||||
|
||||
auto &pool { *m_pools[ Tile.tile.level ] };
|
||||
resident_tile tile;
|
||||
tile.x = Tile.tile.x;
|
||||
tile.z = Tile.tile.z;
|
||||
tile.level = Tile.tile.level;
|
||||
tile.slot = pool.acquire();
|
||||
tile.lastused = m_frame;
|
||||
pool.upload( tile.slot, Tile.heights.data(), Tile.materials.data() );
|
||||
|
||||
if( auto const *description { m_reader.find( Tile.tile.x, Tile.tile.z ) } ) {
|
||||
tile.minheight = description->minheight;
|
||||
tile.maxheight = description->maxheight;
|
||||
}
|
||||
|
||||
++m_stats.uploaded;
|
||||
m_tiles.emplace( tilekey, tile );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::retire( resident_tile const &Tile ) {
|
||||
|
||||
m_pools[ Tile.level ]->release( Tile.slot );
|
||||
return m_table.error( Level );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::detail( double const Pixelsperunit, double const Pixels ) {
|
||||
|
||||
m_finestwanted = terrain_finest_range( m_reader.gridstep(), m_reader.tilesize(), Pixelsperunit, Pixels );
|
||||
m_pixelsperunit = Pixelsperunit;
|
||||
m_detail = std::max( 0.1, Pixels );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::scan( glm::dvec3 const &Viewpoint ) {
|
||||
terrain_clipmap::square( quantizedmesh::tile_address const &Tile, double &West, double &North, double &Side ) const {
|
||||
|
||||
m_scanpoint = Viewpoint;
|
||||
m_scanrange = m_range;
|
||||
m_finest = ( m_finestwanted > 0.0 ? m_finestwanted : terrain_finest_tiles * m_reader.tilesize() );
|
||||
m_inrange.clear();
|
||||
m_wantedlevel.clear();
|
||||
Side = m_table.tilesize( Tile.level );
|
||||
West = m_table.west + Tile.x * Side;
|
||||
North = m_table.north + Tile.z * Side;
|
||||
}
|
||||
|
||||
auto const tilesize { static_cast<double>( m_reader.tilesize() ) };
|
||||
auto const reach { std::max( 1, static_cast<std::int32_t>( m_range / tilesize ) + 1 ) };
|
||||
auto const centrex { m_reader.tile_x( Viewpoint.x ) };
|
||||
auto const centrez { m_reader.tile_z( Viewpoint.z ) };
|
||||
std::vector<std::pair<double, terrain_tile_loader::request>> wishes;
|
||||
// the distance to the nearest point of the tile's square, not to its centre: a coarse tile is
|
||||
// kilometres across, and the one the camera stands on has its centre far outside any draw range
|
||||
double
|
||||
terrain_clipmap::distance_to( quantizedmesh::tile_address const &Tile, glm::dvec3 const &Viewpoint ) const {
|
||||
|
||||
for( std::int32_t z = centrez - reach; z <= centrez + reach; ++z ) {
|
||||
for( std::int32_t x = centrex - reach; x <= centrex + reach; ++x ) {
|
||||
double west { 0.0 }, north { 0.0 }, side { 0.0 };
|
||||
square( Tile, west, north, side );
|
||||
auto const nearestx { std::clamp( Viewpoint.x, west, west + side ) };
|
||||
auto const nearestz { std::clamp( Viewpoint.z, north, north + side ) };
|
||||
return glm::length( glm::dvec2 { nearestx - Viewpoint.x, nearestz - Viewpoint.z } );
|
||||
}
|
||||
|
||||
if( false == m_reader.contains( x, z ) ) { continue; }
|
||||
bool
|
||||
terrain_clipmap::present( quantizedmesh::tile_address const &Tile ) const {
|
||||
|
||||
// distance to the nearest point of the tile, not to its centre. a regional
|
||||
// field has tiles kilometres across, and a tile the camera is standing on
|
||||
// can have its centre far outside the draw range
|
||||
auto const west { x * tilesize };
|
||||
auto const north { z * tilesize };
|
||||
auto const nearestx { std::clamp( Viewpoint.x, west, west + tilesize ) };
|
||||
auto const nearestz { std::clamp( Viewpoint.z, north, north + tilesize ) };
|
||||
auto const distance {
|
||||
glm::length( glm::dvec2 { nearestx - Viewpoint.x, nearestz - Viewpoint.z } ) };
|
||||
if( distance > m_range ) { continue; }
|
||||
return m_reader.present().contains( quantizedmesh::tile_key( Tile ) );
|
||||
}
|
||||
|
||||
auto const tilekey { key( x, z ) };
|
||||
auto const level { level_for( distance ) };
|
||||
m_inrange.push_back( { tilekey, level } );
|
||||
m_wantedlevel.emplace( tilekey, level );
|
||||
// Whether the ground a tile stands for is close enough to the real thing. The tile's error, at its
|
||||
// distance, covers so many pixels; while that is fewer than the detail asks for there is nothing to
|
||||
// be had by splitting it. The tile the camera stands on is always split, whatever its error, so that
|
||||
// what is underfoot is the ground as the scenery drew it.
|
||||
bool
|
||||
terrain_clipmap::good_enough( quantizedmesh::tile_address const &Tile, double const Distance ) const {
|
||||
|
||||
auto const resident { m_tiles.find( tilekey ) };
|
||||
if( ( resident == m_tiles.end() ) || ( resident->second.level != level ) ) {
|
||||
wishes.push_back( { distance, { x, z, level } } );
|
||||
}
|
||||
}
|
||||
if( Tile.level + 1 >= m_levels ) { return true; }
|
||||
if( Distance <= 0.0 ) { return false; }
|
||||
return terrain_level_enough( m_table.error( Tile.level ), Distance, m_pixelsperunit, m_detail );
|
||||
}
|
||||
|
||||
// Walks down to the levels the screen wants and asks the loader for whatever of them is not on the
|
||||
// card, deepest tile of the wanted cut included - not only the tiles one level down. Asking one level
|
||||
// at a time is what a first attempt at this did, and it stops dead: a tile whose children are all
|
||||
// resident asks for nothing, while what it is waiting for is their children.
|
||||
//
|
||||
// Returns whether the cut under this tile, and the tile itself where the cut ends at it, is there. A
|
||||
// tile whose subtree is not ready yet is asked for as well, so that something coarse covers the
|
||||
// ground rather than a hole while the rest is read.
|
||||
bool
|
||||
terrain_clipmap::need( quantizedmesh::tile_address const &Tile, glm::dvec3 const &Viewpoint ) {
|
||||
|
||||
if( false == present( Tile ) ) { return true; } // no ground here to draw
|
||||
|
||||
auto const distance { distance_to( Tile, Viewpoint ) };
|
||||
if( distance > m_range ) { return true; } // nothing wanted this far out
|
||||
|
||||
auto const key { quantizedmesh::tile_key( Tile ) };
|
||||
auto const resident { m_tiles.contains( key ) };
|
||||
|
||||
if( true == good_enough( Tile, distance ) ) {
|
||||
if( false == resident ) { ask_for( Tile, distance ); }
|
||||
m_readiness.emplace( key, resident );
|
||||
return resident;
|
||||
}
|
||||
|
||||
auto ready { true };
|
||||
for( std::int32_t stepz = 0; stepz < 2; ++stepz ) {
|
||||
for( std::int32_t stepx = 0; stepx < 2; ++stepx ) {
|
||||
// every child is asked, so the whole cut is queued at once rather than a level a frame
|
||||
ready = need( { Tile.level + 1, Tile.x * 2 + stepx, Tile.z * 2 + stepz }, Viewpoint ) && ready;
|
||||
}
|
||||
}
|
||||
if( ( false == ready ) && ( false == resident ) ) { ask_for( Tile, distance ); }
|
||||
m_readiness.emplace( key, ready );
|
||||
return ready;
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::choose( quantizedmesh::tile_address const &Tile, glm::dvec3 const &Viewpoint ) {
|
||||
|
||||
if( false == present( Tile ) ) { return; }
|
||||
|
||||
auto const distance { distance_to( Tile, Viewpoint ) };
|
||||
if( distance > m_range ) { return; }
|
||||
|
||||
auto const key { quantizedmesh::tile_key( Tile ) };
|
||||
auto const ready { m_readiness.find( key ) };
|
||||
if( ( ready != m_readiness.end() ) && ( true == ready->second )
|
||||
&& ( false == good_enough( Tile, distance ) ) ) {
|
||||
// the whole cut under this tile is on the card: draw that instead
|
||||
for( std::int32_t stepz = 0; stepz < 2; ++stepz ) {
|
||||
for( std::int32_t stepx = 0; stepx < 2; ++stepx ) {
|
||||
choose( { Tile.level + 1, Tile.x * 2 + stepx, Tile.z * 2 + stepz }, Viewpoint );
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// either this level is good enough, or what is below it is still being read: this tile covers
|
||||
// the ground meanwhile, and nothing under it is drawn, so nothing is drawn twice
|
||||
if( true == m_tiles.contains( key ) ) { m_chosen.push_back( key ); }
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::ask_for( quantizedmesh::tile_address const &Tile, double const Distance ) {
|
||||
|
||||
if( false == m_asked.insert( quantizedmesh::tile_key( Tile ) ).second ) { return; }
|
||||
m_wishes.emplace_back( Distance, Tile );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::walk( glm::dvec3 const &Viewpoint ) {
|
||||
|
||||
m_walkpoint = Viewpoint;
|
||||
m_walkrange = m_range;
|
||||
m_walkdetail = m_detail;
|
||||
m_walked = true;
|
||||
m_chosen.clear();
|
||||
m_wishes.clear();
|
||||
m_asked.clear();
|
||||
m_readiness.clear();
|
||||
|
||||
need( { 0, 0, 0 }, Viewpoint );
|
||||
choose( { 0, 0, 0 }, Viewpoint );
|
||||
|
||||
// nearest first, so the ground under the camera is never waiting behind the horizon
|
||||
std::sort( wishes.begin(), wishes.end(),
|
||||
std::sort( m_wishes.begin(), m_wishes.end(),
|
||||
[]( auto const &Left, auto const &Right ) { return Left.first < Right.first; } );
|
||||
m_wanted.clear();
|
||||
for( auto const &wish : wishes ) { m_wanted.push_back( wish.second ); }
|
||||
m_wanted.reserve( m_wishes.size() );
|
||||
for( auto const &wish : m_wishes ) { m_wanted.push_back( wish.second ); }
|
||||
m_loader.want( m_wanted );
|
||||
|
||||
m_stats.chosen = m_chosen.size();
|
||||
m_stats.wanted = m_wanted.size();
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::put_in( terrain_tile_loader::payload const &Tile ) {
|
||||
|
||||
auto const key { quantizedmesh::tile_key( Tile.data.tile ) };
|
||||
auto const existing { m_tiles.find( key ) };
|
||||
if( existing != m_tiles.end() ) {
|
||||
// already there; nothing about a tile changes between reads
|
||||
++m_stats.dropped;
|
||||
return;
|
||||
}
|
||||
|
||||
auto piece { m_arena.put( Tile.data ) };
|
||||
if( false == piece.held() ) { return; }
|
||||
|
||||
resident_tile tile;
|
||||
tile.piece = piece;
|
||||
tile.centre = glm::dvec3 { Tile.data.centrex, Tile.data.centrey, Tile.data.centrez };
|
||||
tile.radius = static_cast<float>( Tile.data.radius );
|
||||
tile.level = Tile.data.tile.level;
|
||||
tile.lastused = m_frame;
|
||||
m_tiles.emplace( key, tile );
|
||||
++m_stats.uploaded;
|
||||
}
|
||||
|
||||
// What is held is what the walk has been choosing or asking for. Once the buffers are fuller than
|
||||
// the budget, whatever has gone longest without either is given back - never something the walk
|
||||
// wants now, because a coarse tile it is drawing is what stands in for its children while they are
|
||||
// read.
|
||||
void
|
||||
terrain_clipmap::retire_unused() {
|
||||
|
||||
if( m_arena.bytes() <= m_budget ) { return; }
|
||||
|
||||
std::vector<std::pair<std::uint64_t, std::int64_t>> aged;
|
||||
aged.reserve( m_tiles.size() );
|
||||
for( auto const &[ key, tile ] : m_tiles ) {
|
||||
if( tile.lastused != m_frame ) { aged.emplace_back( tile.lastused, key ); }
|
||||
}
|
||||
if( true == aged.empty() ) { return; }
|
||||
std::sort( aged.begin(), aged.end() );
|
||||
|
||||
// a fifth of what is held, so that this does not run every frame
|
||||
auto const letgo { std::max<std::size_t>( 1, m_tiles.size() / 5 ) };
|
||||
for( std::size_t index = 0; ( index < letgo ) && ( index < aged.size() ); ++index ) {
|
||||
auto const found { m_tiles.find( aged[ index ].second ) };
|
||||
if( found == m_tiles.end() ) { continue; }
|
||||
m_arena.take( found->second.piece );
|
||||
m_tiles.erase( found );
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::update( glm::dvec3 const &Viewpoint ) {
|
||||
|
||||
if( false == m_ready ) { return; }
|
||||
|
||||
++m_frame;
|
||||
|
||||
auto const moved {
|
||||
glm::length( glm::dvec2 { Viewpoint.x - m_scanpoint.x, Viewpoint.z - m_scanpoint.z } ) };
|
||||
// a zoom or a resized window changes the levels; a change under a percent is not worth
|
||||
// redoing the scan for
|
||||
auto const detailchanged {
|
||||
( m_finestwanted > 0.0 ) && ( std::abs( m_finestwanted - m_finest ) > 0.01 * m_finest ) };
|
||||
if( ( m_range != m_scanrange )
|
||||
|| ( true == detailchanged )
|
||||
|| ( moved > m_reader.tilesize() * terrain_rescan_tiles ) ) {
|
||||
scan( Viewpoint );
|
||||
}
|
||||
|
||||
// the tiles in range are touched every frame, which is what render() draws and what
|
||||
// eviction spares
|
||||
m_stats.inview = 0;
|
||||
m_stats.wanted = 0;
|
||||
m_stats.perlevel.fill( 0 );
|
||||
for( auto const &tile : m_inrange ) {
|
||||
auto const resident { m_tiles.find( tile.key ) };
|
||||
if( resident == m_tiles.end() ) {
|
||||
++m_stats.wanted;
|
||||
continue;
|
||||
}
|
||||
// still wanted, at whatever level it has until the right one arrives
|
||||
resident->second.lastused = m_frame;
|
||||
++m_stats.inview;
|
||||
++m_stats.perlevel[ std::min<std::size_t>( resident->second.level, m_stats.perlevel.size() - 1 ) ];
|
||||
if( resident->second.level != tile.level ) { ++m_stats.wanted; }
|
||||
}
|
||||
|
||||
m_arrived.clear();
|
||||
m_loader.collect( m_arrived, m_uploadsperframe );
|
||||
m_loader.collect( m_arrived, m_putsperframe );
|
||||
for( auto const &tile : m_arrived ) {
|
||||
if( false == tile.valid ) { continue; }
|
||||
// a tile read for where the camera was is put up only if nothing of it is on the gpu
|
||||
// yet - some level is better than a hole. otherwise it is dropped, and the level
|
||||
// a tile read for where the camera was is still put in: it covers ground, and what is
|
||||
// wanted now is already queued
|
||||
auto const tilekey { key( tile.tile.x, tile.tile.z ) };
|
||||
auto const wanted { m_wantedlevel.find( tilekey ) };
|
||||
auto const stillwanted { ( wanted != m_wantedlevel.end() ) && ( wanted->second == tile.tile.level ) };
|
||||
if( stillwanted || ( false == m_tiles.contains( tilekey ) ) ) {
|
||||
upload( tile );
|
||||
// put up this frame, so drawn this frame
|
||||
m_tiles.find( tilekey )->second.lastused = m_frame;
|
||||
}
|
||||
else {
|
||||
++m_stats.dropped;
|
||||
}
|
||||
put_in( tile );
|
||||
}
|
||||
|
||||
if( ( m_tiles.size() > m_budget ) && ( m_tiles.size() > m_stats.inview ) ) {
|
||||
// over budget, and not everything is in view: drop whatever has gone longest without
|
||||
// being asked for
|
||||
std::vector<std::pair<std::uint64_t, std::int64_t>> aged;
|
||||
for( auto const & [ tilekey, tile ] : m_tiles ) {
|
||||
if( tile.lastused != m_frame ) { aged.emplace_back( tile.lastused, tilekey ); }
|
||||
}
|
||||
auto const excess { std::min( aged.size(), m_tiles.size() - m_budget ) };
|
||||
std::partial_sort( aged.begin(), aged.begin() + excess, aged.end() );
|
||||
for( std::size_t index = 0; index < excess; ++index ) {
|
||||
auto const victim { m_tiles.find( aged[ index ].second ) };
|
||||
retire( victim->second );
|
||||
m_tiles.erase( victim );
|
||||
}
|
||||
auto const moved {
|
||||
glm::length( glm::dvec2 { Viewpoint.x - m_walkpoint.x, Viewpoint.z - m_walkpoint.z } ) };
|
||||
auto const detailchanged { std::abs( m_detail - m_walkdetail ) > 0.01 * m_walkdetail };
|
||||
if( ( false == m_walked )
|
||||
|| ( m_range != m_walkrange )
|
||||
|| ( true == detailchanged )
|
||||
|| ( moved > m_finesttile * terrain_rescan_tiles )
|
||||
|| ( false == m_arrived.empty() ) ) {
|
||||
// a tile that has just arrived lets the walk go deeper where it was drawing a coarse one
|
||||
walk( Viewpoint );
|
||||
}
|
||||
|
||||
// what the walk settled on is wanted now, and is what eviction spares
|
||||
m_stats.perlevel.fill( 0 );
|
||||
for( auto const key : m_chosen ) {
|
||||
auto const found { m_tiles.find( key ) };
|
||||
if( found == m_tiles.end() ) { continue; }
|
||||
found->second.lastused = m_frame;
|
||||
++m_stats.perlevel[ std::min<std::size_t>( found->second.level, m_stats.perlevel.size() - 1 ) ];
|
||||
}
|
||||
|
||||
retire_unused();
|
||||
m_stats.resident = m_tiles.size();
|
||||
m_stats.texturebytes = 0;
|
||||
for( auto const &pool : m_pools ) { m_stats.texturebytes += pool->bytes(); }
|
||||
m_stats.gpubytes = m_arena.bytes();
|
||||
report_residency();
|
||||
}
|
||||
|
||||
|
||||
// residency settles within a second of the camera stopping and then barely moves, so a
|
||||
// line is written when it shifts by a noticeable amount rather than every frame. that
|
||||
// makes the warm figure - what the terrain actually costs - readable from the log
|
||||
// residency settles within a second of the camera stopping and then barely moves, so a line is
|
||||
// written when it shifts by a noticeable amount rather than every frame. that makes the warm figure
|
||||
// - what the terrain actually costs - readable from the log
|
||||
void
|
||||
terrain_clipmap::report_residency() {
|
||||
|
||||
auto const difference {
|
||||
m_stats.resident > m_reportedresidency ?
|
||||
m_stats.resident - m_reportedresidency :
|
||||
m_reportedresidency - m_stats.resident };
|
||||
if( ( m_reported == true ) && ( difference < 16 ) ) { return; }
|
||||
auto const apart {
|
||||
[]( std::size_t const Left, std::size_t const Right ) {
|
||||
return Left > Right ? Left - Right : Right - Left; } };
|
||||
auto const moved {
|
||||
( apart( m_stats.resident, m_reportedresidency ) >= 8 )
|
||||
|| ( apart( m_stats.drawn, m_reporteddrawn ) >= 4 ) };
|
||||
// at most once every couple of seconds, and only when the picture has actually changed: what
|
||||
// this is for is the warm figure, what the terrain settles at, not a line per frame
|
||||
if( ( m_reported == true ) && ( ( false == moved ) || ( m_frame < m_reportedframe + 120 ) ) ) { return; }
|
||||
|
||||
m_reported = true;
|
||||
m_reportedresidency = m_stats.resident;
|
||||
m_reporteddrawn = m_stats.drawn;
|
||||
m_reportedframe = m_frame;
|
||||
WriteLog(
|
||||
"Terrain: " + std::to_string( m_stats.resident ) + " tiles resident, "
|
||||
+ std::to_string( m_stats.texturebytes / 1048576 ) + " MB of samples, "
|
||||
+ std::to_string( m_stats.uploaded ) + " uploads so far, "
|
||||
+ std::to_string( m_stats.drawn ) + " of " + std::to_string( m_stats.inview ) + " in range drawn, finest level out to "
|
||||
+ std::to_string( static_cast<int>( m_finest ) ) + " m" );
|
||||
+ std::to_string( m_stats.gpubytes / 1048576 ) + " MB of mesh, "
|
||||
+ std::to_string( m_stats.uploaded ) + " put in so far, "
|
||||
+ std::to_string( m_stats.drawn ) + " of " + std::to_string( m_stats.chosen ) + " chosen drawn, "
|
||||
+ std::to_string( m_stats.triangles / 1000 ) + "k triangles, levels "
|
||||
+ [ this ]() {
|
||||
std::string counts;
|
||||
for( std::uint32_t level = 0; level < m_levels; ++level ) {
|
||||
counts += ( level > 0 ? "/" : "" ) + std::to_string( m_stats.perlevel[ level ] );
|
||||
}
|
||||
return counts; }() );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::render( glm::dvec3 const &Viewpoint, visibility const &Visible ) {
|
||||
|
||||
if( ( false == m_ready ) || ( true == m_tiles.empty() ) ) { return; }
|
||||
if( ( false == m_ready ) || ( true == m_chosen.empty() ) ) { return; }
|
||||
|
||||
// textures that finished loading since the last frame go into the ground array first
|
||||
// the ground textures the engine has finished loading since the last frame
|
||||
m_ground.update();
|
||||
|
||||
// what each level draws: the tiles touched by the last update, and seen. a tile's bounding
|
||||
// sphere holds the whole tile, morphing included, since morphing only moves vertices
|
||||
// towards samples of the same tile
|
||||
auto const tilesize { static_cast<double>( m_reader.tilesize() ) };
|
||||
for( auto &instances : m_instances ) { instances.clear(); }
|
||||
for( auto const & [ tilekey, tile ] : m_tiles ) {
|
||||
if( tile.lastused != m_frame ) { continue; }
|
||||
auto const halfheight { 0.5 * ( tile.maxheight - tile.minheight ) };
|
||||
glm::dvec3 const centre {
|
||||
( tile.x + 0.5 ) * tilesize, 0.5 * ( tile.minheight + tile.maxheight ), ( tile.z + 0.5 ) * tilesize };
|
||||
auto const radius {
|
||||
static_cast<float>( std::sqrt( 0.5 * tilesize * tilesize + halfheight * halfheight ) ) };
|
||||
if( false == Visible( centre, radius ) ) { continue; }
|
||||
// relative to the viewpoint, so that the shader never sees a coordinate large enough
|
||||
// for single precision to matter
|
||||
m_instances[ tile.level ].insert(
|
||||
m_instances[ tile.level ].end(),
|
||||
{ static_cast<float>( tile.x * tilesize - Viewpoint.x ),
|
||||
static_cast<float>( -Viewpoint.y ),
|
||||
static_cast<float>( tile.z * tilesize - Viewpoint.z ),
|
||||
static_cast<float>( tile.slot ),
|
||||
static_cast<float>( tile.x * tilesize ),
|
||||
static_cast<float>( tile.z * tilesize ),
|
||||
0.f, 0.f } );
|
||||
m_counts.clear();
|
||||
m_offsets.clear();
|
||||
m_bases.clear();
|
||||
std::size_t triangles { 0 };
|
||||
for( auto const key : m_chosen ) {
|
||||
auto const found { m_tiles.find( key ) };
|
||||
if( found == m_tiles.end() ) { continue; }
|
||||
auto const &tile { found->second };
|
||||
if( false == Visible( tile.centre, tile.radius ) ) { continue; }
|
||||
m_counts.push_back( static_cast<std::int32_t>( tile.piece.indices ) );
|
||||
m_offsets.push_back(
|
||||
reinterpret_cast<void const *>(
|
||||
static_cast<std::uintptr_t>( tile.piece.firstindex ) * sizeof( std::uint32_t ) ) );
|
||||
m_bases.push_back( static_cast<std::int32_t>( tile.piece.firstvertex ) );
|
||||
triangles += tile.piece.indices / 3;
|
||||
}
|
||||
m_stats.drawn = m_counts.size();
|
||||
m_stats.triangles = triangles;
|
||||
if( true == m_counts.empty() ) { return; }
|
||||
|
||||
m_shader->bind();
|
||||
m_vao->bind();
|
||||
// the depth buffer is reversed - nearer is greater - so pushing back is a negative offset
|
||||
::glEnable( GL_POLYGON_OFFSET_FILL );
|
||||
::glPolygonOffset( -1.f - 2.f * static_cast<float>( m_depthrank ), -2.f - 4.f * static_cast<float>( m_depthrank ) );
|
||||
|
||||
::glUniform1f( m_uniforms.heightbias, m_reader.height( 0 ) );
|
||||
::glUniform1f( m_uniforms.heightscale, m_reader.height( 1 ) - m_reader.height( 0 ) );
|
||||
::glUniform1ui( m_uniforms.nodata, heightfield::nodata );
|
||||
::glUniform1i( m_uniforms.heights, 0 );
|
||||
glm::vec3 const origin {
|
||||
static_cast<float>( m_origin.x - Viewpoint.x ),
|
||||
static_cast<float>( m_origin.y - Viewpoint.y ),
|
||||
static_cast<float>( m_origin.z - Viewpoint.z ) };
|
||||
::glUniform3fv( m_uniforms.origin, 1, &origin.x );
|
||||
glm::vec2 const world { static_cast<float>( m_origin.x ), static_cast<float>( m_origin.z ) };
|
||||
::glUniform2fv( m_uniforms.world, 1, &world.x );
|
||||
::glUniform1i( m_uniforms.ground, 0 );
|
||||
::glUniform1i( m_uniforms.materials, 1 );
|
||||
::glUniform1i( m_uniforms.ground, 2 );
|
||||
::glUniform1i( m_uniforms.materialtable, 3 );
|
||||
::glUniform1i( m_uniforms.instances, 4 );
|
||||
|
||||
::glActiveTexture( GL_TEXTURE2 );
|
||||
::glActiveTexture( GL_TEXTURE0 );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, m_ground.textures() );
|
||||
::glActiveTexture( GL_TEXTURE3 );
|
||||
::glActiveTexture( GL_TEXTURE1 );
|
||||
::glBindTexture( GL_TEXTURE_2D, m_ground.table() );
|
||||
|
||||
std::size_t drawn { 0 };
|
||||
auto const gridstep { m_reader.gridstep() };
|
||||
for( std::uint32_t level = 0; level < m_instances.size(); ++level ) {
|
||||
// the ground loses the depth test to whatever lies on it, and a coarser terrain to a finer one
|
||||
// describing the same ground. the offset is negative because depth runs the other way here
|
||||
::glEnable( GL_POLYGON_OFFSET_FILL );
|
||||
::glPolygonOffset(
|
||||
-1.f - 2.f * static_cast<float>( m_depthrank ),
|
||||
-2.f - 4.f * static_cast<float>( m_depthrank ) );
|
||||
|
||||
auto const &instances { m_instances[ level ] };
|
||||
if( true == instances.empty() ) { continue; }
|
||||
auto const count { instances.size() / 8 };
|
||||
drawn += count;
|
||||
|
||||
m_instancebuffer->allocate(
|
||||
gl::buffer::TEXTURE_BUFFER, static_cast<GLsizeiptr>( instances.size() * sizeof( float ) ), GL_STREAM_DRAW );
|
||||
m_instancebuffer->upload(
|
||||
gl::buffer::TEXTURE_BUFFER, instances.data(), 0, static_cast<GLsizeiptr>( instances.size() * sizeof( float ) ) );
|
||||
::glActiveTexture( GL_TEXTURE4 );
|
||||
::glBindTexture( GL_TEXTURE_BUFFER, m_instancetexture );
|
||||
::glTexBuffer( GL_TEXTURE_BUFFER, GL_RGBA32F, static_cast<GLuint>( *m_instancebuffer ) );
|
||||
|
||||
auto const &pool { *m_pools[ level ] };
|
||||
::glActiveTexture( GL_TEXTURE0 );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, pool.heights() );
|
||||
::glActiveTexture( GL_TEXTURE1 );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, pool.materials() );
|
||||
|
||||
::glUniform1f( m_uniforms.samplestep, gridstep * static_cast<float>( 1u << level ) );
|
||||
::glUniform1i( m_uniforms.side, static_cast<GLint>( pool.side() ) );
|
||||
// the band over which this level turns into the next. the coarsest level has no next
|
||||
// one, so its band lies beyond anything that is ever drawn
|
||||
if( level + 1 < m_reader.levels() ) {
|
||||
::glUniform2f(
|
||||
m_uniforms.morph,
|
||||
static_cast<float>( terrain_morph_begin( level, m_finest, tilesize ) ),
|
||||
static_cast<float>( terrain_morph_end( level, m_finest, tilesize ) ) );
|
||||
}
|
||||
else {
|
||||
::glUniform2f( m_uniforms.morph, 1e30f, 2e30f );
|
||||
}
|
||||
|
||||
m_indices[ level ]->bind( gl::buffer::ELEMENT_ARRAY_BUFFER );
|
||||
::glDrawElementsInstanced(
|
||||
GL_TRIANGLES, static_cast<GLsizei>( m_indexcounts[ level ] ), GL_UNSIGNED_INT, nullptr,
|
||||
static_cast<GLsizei>( count ) );
|
||||
}
|
||||
m_stats.drawn = drawn;
|
||||
::glBindVertexArray( m_arena.vertexarray() );
|
||||
::glMultiDrawElementsBaseVertex(
|
||||
GL_TRIANGLES, m_counts.data(), GL_UNSIGNED_INT,
|
||||
const_cast<void const **>( m_offsets.data() ),
|
||||
static_cast<GLsizei>( m_counts.size() ), m_bases.data() );
|
||||
::glBindVertexArray( 0 );
|
||||
|
||||
::glDisable( GL_POLYGON_OFFSET_FILL );
|
||||
gl::buffer::unbind( gl::buffer::TEXTURE_BUFFER );
|
||||
::glActiveTexture( GL_TEXTURE1 );
|
||||
::glBindTexture( GL_TEXTURE_2D, 0 );
|
||||
::glActiveTexture( GL_TEXTURE0 );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, 0 );
|
||||
opengl_texture::reset_unit_cache();
|
||||
m_vao->unbind();
|
||||
gl::program::bind( 0 );
|
||||
}
|
||||
|
||||
@@ -11,37 +11,35 @@ module;
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "scene/quantizedmeshreader.h"
|
||||
|
||||
export module eu07.rendering.terrainclipmap;
|
||||
import eu07.gl.shader;
|
||||
import eu07.gl.buffer;
|
||||
import eu07.gl.vao;
|
||||
import eu07.glm;
|
||||
import eu07.rendering.terraintileloader;
|
||||
import eu07.rendering.terrainstatus;
|
||||
import eu07.rendering.terraintilepool_tin;
|
||||
import eu07.rendering.terrainmesharena;
|
||||
import eu07.rendering.terrainground;
|
||||
|
||||
export {
|
||||
|
||||
// Draws TIN (Triangulated Irregular Network) terrain from Quantized Mesh tiles.
|
||||
// Draws a scenery's cooked terrain: a quadtree of mesh tiles, each a piece of the ground as the
|
||||
// scenery drew it, thinned out the further from the camera it is drawn.
|
||||
//
|
||||
// Replaces the old heightfield texture array approach with vertex/index buffers per tile.
|
||||
// Each tile is an independent triangulated mesh with multiple LOD levels. The terrain adapts
|
||||
// density to detail: fine triangulation along track corridors, coarse in open fields.
|
||||
// Each frame a walk of the quadtree settles on the tiles to draw: a tile is split when the ground
|
||||
// it stands for would be off by more pixels than the detail setting allows, and only when all four
|
||||
// of its children are on the card - otherwise it is drawn itself and the children are asked for, so
|
||||
// that streaming shows coarse ground rather than a hole. Because every vertex on a tile's border
|
||||
// survives into all of its coarser levels, two neighbours drawn at different levels share their
|
||||
// border exactly, and nothing has to be stitched or morphed.
|
||||
//
|
||||
// CDLOD (Continuous Distance LOD): tiles select their LOD based on distance from camera,
|
||||
// with edge stitching to prevent cracks between LOD transitions.
|
||||
//
|
||||
// The class owns GL resources and file handles. Given a viewpoint and visibility test,
|
||||
// it loads, uploads, and renders appropriate tiles each frame.
|
||||
// Every resident tile lives in the same pair of buffers, so the whole terrain is drawn in one call.
|
||||
class terrain_clipmap {
|
||||
|
||||
public:
|
||||
@@ -53,116 +51,121 @@ public:
|
||||
terrain_clipmap( terrain_clipmap const & ) = delete;
|
||||
terrain_clipmap &operator=( terrain_clipmap const & ) = delete;
|
||||
|
||||
// opens a cooked TIN directory and builds the gl resources. returns false when the
|
||||
// directory cannot be read, leaving the object unusable but safe to destroy
|
||||
// opens a cooked terrain - an archive or a directory of tiles - and builds the gl resources.
|
||||
// returns false when it cannot be read, leaving the object unusable but safe to destroy
|
||||
bool open( std::string const &Path );
|
||||
bool ready() const { return m_ready; }
|
||||
void close();
|
||||
|
||||
// asks for the tiles around the viewpoint, uploads a bounded number of those that have
|
||||
// arrived, and retires the ones that left range. safe to call every frame: the reading
|
||||
// and decoding happen on the loader's thread
|
||||
// walks the quadtree, asks for what is missing, puts in what has arrived and retires what has
|
||||
// not been wanted for a while. safe to call every frame: the reading happens on the loader's
|
||||
// thread
|
||||
void update( glm::dvec3 const &Viewpoint );
|
||||
// draws the resident tiles in range that the test says can be seen. the caller is
|
||||
// expected to have the scene and model uniform blocks already set for a camera-relative
|
||||
// pass. tile positions are passed as uniforms per draw
|
||||
// draws the tiles the walk settled on that the test says can be seen
|
||||
void render( glm::dvec3 const &Viewpoint, visibility const &Visible );
|
||||
|
||||
// how far from the viewpoint terrain is drawn, in metres
|
||||
void range( float const Range ) { m_range = Range; }
|
||||
float range() const { return m_range; }
|
||||
// what the screen can resolve: the viewport's pixels per unit of size over distance, and
|
||||
// how many pixels a sample spacing may cover. decides how far each level reaches
|
||||
// what the screen can resolve: the viewport's pixels per unit of size over distance, and how
|
||||
// many pixels a tile's error may cover before a finer level is used
|
||||
void detail( double const Pixelsperunit, double const Pixels );
|
||||
// the finest level's range the tiles in range were last worked out with
|
||||
float finest() const { return static_cast<float>( m_finest ); }
|
||||
|
||||
// metres between samples at the finest level; a scenery's fields are drawn from the
|
||||
// finest to the coarsest so that the depth test keeps the better surface
|
||||
float gridstep() const { return m_reader.gridstep(); }
|
||||
// pushes this field back in depth by the given rank, so that geometry lying on it wins the
|
||||
// depth test, and a coarser field loses to a finer one describing the same ground. the
|
||||
// offset is in depth, scaled by slope, so it holds at any distance and on any incline
|
||||
// side of a tile at the finest level, in metres. terrains are drawn finest first, so that the
|
||||
// depth test keeps the better surface where two of them describe the same ground
|
||||
float gridstep() const { return m_finesttile; }
|
||||
// pushes this terrain back in depth by the given rank, so that geometry lying on it wins the
|
||||
// depth test, and a coarser terrain loses to a finer one describing the same ground
|
||||
void depthrank( unsigned const Rank ) { m_depthrank = Rank; }
|
||||
|
||||
terrain_statistics const &stats() const { return m_stats; }
|
||||
std::string const &path() const { return m_path; }
|
||||
// metres per tile side
|
||||
float tilesize() const { return m_tilesize; }
|
||||
// number of LOD levels available
|
||||
std::uint32_t levels() const { return m_lodlevels; }
|
||||
float tilesize() const { return m_finesttile; }
|
||||
std::uint32_t levels() const { return m_levels; }
|
||||
std::size_t backlog() const { return m_loader.backlog(); }
|
||||
float detailpixels() const { return static_cast<float>( m_detail ); }
|
||||
// side of a tile at each level, and how far that level's ground is off, for the debug panel
|
||||
float leveltile( std::uint32_t const Level ) const;
|
||||
float levelerror( std::uint32_t const Level ) const;
|
||||
|
||||
private:
|
||||
// one tile resident on the gpu: VBO/IBO handle
|
||||
// one tile held in the buffers
|
||||
struct resident_tile {
|
||||
std::int32_t x { 0 }, z { 0 };
|
||||
terrain_mesh_arena::piece piece;
|
||||
// for culling, in world space
|
||||
glm::dvec3 centre { 0.0 };
|
||||
float radius { 0.f };
|
||||
std::uint32_t level { 0 };
|
||||
terrain_tile_pool_tin::tile_handle gpu_data;
|
||||
float minheight { 0.f }, maxheight { 0.f };
|
||||
std::uint64_t lastused { 0 };
|
||||
};
|
||||
|
||||
// one tile within range, and the level its distance earns
|
||||
struct in_range {
|
||||
std::int64_t key;
|
||||
std::uint32_t level;
|
||||
};
|
||||
|
||||
// mip level a tile at the given distance is drawn at
|
||||
std::uint32_t level_for( double const Distance ) const;
|
||||
// works out which tiles are in range and at what level, and tells the loader what is
|
||||
// missing. the answer only changes once the camera has moved a fair part of a tile
|
||||
void scan( glm::dvec3 const &Viewpoint );
|
||||
// puts a tile the loader has read on the gpu, replacing whatever level it had before
|
||||
void upload( terrain_tile_loader::payload const &Tile );
|
||||
void retire( resident_tile const &Tile );
|
||||
// the tiles the walk settled on, in the order they will be drawn
|
||||
void walk( glm::dvec3 const &Viewpoint );
|
||||
// asks the loader for every tile the wanted cut of the quadtree is missing, and says whether this
|
||||
// tile and everything under it is already there
|
||||
bool need( quantizedmesh::tile_address const &Tile, glm::dvec3 const &Viewpoint );
|
||||
// whether the level this tile stands for is close enough to the ground below it
|
||||
bool good_enough( quantizedmesh::tile_address const &Tile, double Distance ) const;
|
||||
void choose( quantizedmesh::tile_address const &Tile, glm::dvec3 const &Viewpoint );
|
||||
// where a tile's square is, and how far the viewpoint is from it
|
||||
void square( quantizedmesh::tile_address const &Tile, double &West, double &North, double &Side ) const;
|
||||
double distance_to( quantizedmesh::tile_address const &Tile, glm::dvec3 const &Viewpoint ) const;
|
||||
bool present( quantizedmesh::tile_address const &Tile ) const;
|
||||
void ask_for( quantizedmesh::tile_address const &Tile, double Distance );
|
||||
void put_in( terrain_tile_loader::payload const &Tile );
|
||||
void retire_unused();
|
||||
void resolve_uniforms();
|
||||
void report_residency();
|
||||
|
||||
static std::int64_t key( std::int32_t const X, std::int32_t const Z ) {
|
||||
return ( static_cast<std::int64_t>( X ) << 32 ) ^ static_cast<std::uint32_t>( Z );
|
||||
}
|
||||
|
||||
// Tile management
|
||||
quantizedmesh::reader m_reader;
|
||||
quantizedmesh::reader m_reader; // the table and which tiles there are; reading is the loader's
|
||||
terrain_tile_loader m_loader;
|
||||
terrain_tile_pool_tin m_pool;
|
||||
|
||||
std::vector<terrain_tile_loader::request> m_wanted;
|
||||
std::vector<terrain_tile_loader::payload> m_arrived;
|
||||
// what the last scan found in range, and the level wanted for each tile
|
||||
std::vector<in_range> m_inrange;
|
||||
std::unordered_map<std::int64_t, std::uint32_t> m_wantedlevel;
|
||||
glm::dvec3 m_scanpoint { 0.0 };
|
||||
float m_scanrange { -1.f };
|
||||
double m_finestwanted { 0.0 };
|
||||
double m_finest { 0.0 };
|
||||
|
||||
// Resident tiles (key = tile_key(x,z))
|
||||
std::unordered_map<std::int64_t, resident_tile> m_tiles;
|
||||
|
||||
// Ground texture array (materials)
|
||||
terrain_mesh_arena m_arena;
|
||||
terrain_ground m_ground;
|
||||
|
||||
// Shader and uniforms
|
||||
|
||||
quantizedmesh::terrain_table m_table;
|
||||
glm::dvec3 m_origin { 0.0 }; // what the vertices in the buffers are measured from
|
||||
float m_finesttile { 256.f };
|
||||
std::uint32_t m_levels { 1 };
|
||||
|
||||
std::unordered_map<std::int64_t, resident_tile> m_tiles;
|
||||
std::vector<std::int64_t> m_chosen;
|
||||
std::vector<std::pair<double, quantizedmesh::tile_address>> m_wishes;
|
||||
std::vector<quantizedmesh::tile_address> m_wanted;
|
||||
std::vector<terrain_tile_loader::payload> m_arrived;
|
||||
std::unordered_set<std::int64_t> m_asked; // what the last walk asked the loader for
|
||||
// whether a subtree could be drawn, worked out once per walk
|
||||
std::unordered_map<std::int64_t, bool> m_readiness;
|
||||
|
||||
// what the draw hands to gl, kept between frames so that a frame allocates nothing
|
||||
std::vector<std::int32_t> m_counts;
|
||||
std::vector<void const *> m_offsets;
|
||||
std::vector<std::int32_t> m_bases;
|
||||
|
||||
glm::dvec3 m_walkpoint { 0.0 };
|
||||
bool m_walked { false };
|
||||
double m_pixelsperunit { 0.0 };
|
||||
double m_detail { 3.0 };
|
||||
double m_walkdetail { 0.0 };
|
||||
float m_range { 6000.f };
|
||||
float m_walkrange { -1.f };
|
||||
|
||||
std::optional<gl::program> m_shader;
|
||||
struct uniform_locations {
|
||||
int tile_offset { -1 }; // vec3: tile origin relative to camera
|
||||
int ground { -1 }; // sampler2DArray: material textures
|
||||
int origin { -1 }; // where the terrain's origin is, relative to the camera
|
||||
int world { -1 }; // and where it is in the world
|
||||
int ground { -1 };
|
||||
int materials { -1 };
|
||||
} m_uniforms;
|
||||
|
||||
// Configuration
|
||||
float m_tilesize { 256.0f };
|
||||
std::uint32_t m_lodlevels { 4 };
|
||||
terrain_statistics m_stats;
|
||||
std::uint64_t m_frame { 0 };
|
||||
std::size_t m_reportedresidency { 0 };
|
||||
std::size_t m_reporteddrawn { 0 };
|
||||
std::uint64_t m_reportedframe { 0 };
|
||||
bool m_reported { false };
|
||||
float m_range { 6000.f };
|
||||
unsigned m_depthrank { 0 };
|
||||
std::size_t m_budget { 2048 }; // tiles allowed resident
|
||||
std::size_t m_uploadsperframe { 32 };
|
||||
// what the buffers may hold before tiles nobody asked for lately are let go
|
||||
std::size_t m_budget { 192u << 20 };
|
||||
std::size_t m_putsperframe { 16 };
|
||||
bool m_ready { false };
|
||||
std::string m_path;
|
||||
};
|
||||
|
||||
255
rendering/terrainmesharena.cpp
Normal file
255
rendering/terrainmesharena.cpp
Normal file
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
module;
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <glad/glad.h>
|
||||
#include "scene/quantizedmeshreader.h"
|
||||
|
||||
module eu07.rendering.terrainmesharena;
|
||||
import eu07.utilities.logs;
|
||||
|
||||
namespace {
|
||||
|
||||
// what the buffers start at, in vertices and indices, and the most they may reach. a vertex is 24
|
||||
// bytes and an index 4, so the ceiling is about 400 MB of terrain on the card
|
||||
constexpr std::uint32_t firstvertices { 1u << 19 };
|
||||
constexpr std::uint32_t firstindices { 1u << 21 };
|
||||
constexpr std::uint32_t mostvertices { 1u << 24 };
|
||||
constexpr std::uint32_t mostindices { 1u << 26 };
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void
|
||||
terrain_mesh_arena::span_list::give( std::uint32_t const At, std::uint32_t const Count ) {
|
||||
|
||||
if( Count == 0 ) { return; }
|
||||
auto placed { m_free.emplace( At, Count ).first };
|
||||
// join what now touches, so that the room a tile gave back can hold a larger one
|
||||
auto after { std::next( placed ) };
|
||||
if( ( after != m_free.end() ) && ( placed->first + placed->second == after->first ) ) {
|
||||
placed->second += after->second;
|
||||
m_free.erase( after );
|
||||
}
|
||||
if( placed != m_free.begin() ) {
|
||||
auto before { std::prev( placed ) };
|
||||
if( before->first + before->second == placed->first ) {
|
||||
before->second += placed->second;
|
||||
m_free.erase( placed );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
terrain_mesh_arena::span_list::take( std::uint32_t const Count, std::uint32_t &At ) {
|
||||
|
||||
for( auto span = m_free.begin(); span != m_free.end(); ++span ) {
|
||||
if( span->second < Count ) { continue; }
|
||||
At = span->first;
|
||||
if( span->second == Count ) { m_free.erase( span ); }
|
||||
else {
|
||||
auto const left { span->second - Count };
|
||||
m_free.erase( span );
|
||||
m_free.emplace( At + Count, left );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
terrain_mesh_arena::~terrain_mesh_arena() {
|
||||
|
||||
destroy();
|
||||
}
|
||||
|
||||
bool
|
||||
terrain_mesh_arena::create() {
|
||||
|
||||
destroy();
|
||||
::glGenVertexArrays( 1, &m_vao );
|
||||
::glGenBuffers( 1, &m_vertexbuffer );
|
||||
::glGenBuffers( 1, &m_indexbuffer );
|
||||
if( ( m_vao == 0 ) || ( m_vertexbuffer == 0 ) || ( m_indexbuffer == 0 ) ) { destroy(); return false; }
|
||||
|
||||
::glBindVertexArray( m_vao );
|
||||
::glBindBuffer( GL_ARRAY_BUFFER, m_vertexbuffer );
|
||||
::glBufferData( GL_ARRAY_BUFFER,
|
||||
static_cast<GLsizeiptr>( firstvertices ) * sizeof( quantizedmesh::render_vertex ), nullptr, GL_DYNAMIC_DRAW );
|
||||
::glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, m_indexbuffer );
|
||||
::glBufferData( GL_ELEMENT_ARRAY_BUFFER,
|
||||
static_cast<GLsizeiptr>( firstindices ) * sizeof( std::uint32_t ), nullptr, GL_DYNAMIC_DRAW );
|
||||
|
||||
auto const stride { static_cast<GLsizei>( sizeof( quantizedmesh::render_vertex ) ) };
|
||||
::glEnableVertexAttribArray( 0 );
|
||||
::glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride,
|
||||
reinterpret_cast<void const *>( offsetof( quantizedmesh::render_vertex, x ) ) );
|
||||
::glEnableVertexAttribArray( 1 );
|
||||
::glVertexAttribIPointer( 1, 1, GL_UNSIGNED_SHORT, stride,
|
||||
reinterpret_cast<void const *>( offsetof( quantizedmesh::render_vertex, material ) ) );
|
||||
::glBindVertexArray( 0 );
|
||||
::glBindBuffer( GL_ARRAY_BUFFER, 0 );
|
||||
::glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
|
||||
|
||||
m_vertexroom = firstvertices;
|
||||
m_indexroom = firstindices;
|
||||
m_freevertices.clear();
|
||||
m_freeindices.clear();
|
||||
m_freevertices.give( 0, m_vertexroom );
|
||||
m_freeindices.give( 0, m_indexroom );
|
||||
m_verticesheld = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
terrain_mesh_arena::destroy() {
|
||||
|
||||
if( m_vao != 0 ) { ::glDeleteVertexArrays( 1, &m_vao ); m_vao = 0; }
|
||||
if( m_vertexbuffer != 0 ) { ::glDeleteBuffers( 1, &m_vertexbuffer ); m_vertexbuffer = 0; }
|
||||
if( m_indexbuffer != 0 ) { ::glDeleteBuffers( 1, &m_indexbuffer ); m_indexbuffer = 0; }
|
||||
m_vertexroom = m_indexroom = 0;
|
||||
m_freevertices.clear();
|
||||
m_freeindices.clear();
|
||||
m_verticesheld = 0;
|
||||
}
|
||||
|
||||
// Growing copies what is there into a larger buffer on the card: the pieces keep their places, so
|
||||
// nothing has to be read back or re-uploaded.
|
||||
bool
|
||||
terrain_mesh_arena::grow_vertices( std::uint32_t const Wanted ) {
|
||||
|
||||
auto room { m_vertexroom };
|
||||
while( ( room < m_vertexroom + Wanted ) && ( room < mostvertices ) ) { room *= 2; }
|
||||
if( room <= m_vertexroom ) { return false; }
|
||||
|
||||
std::uint32_t grown { 0 };
|
||||
::glGenBuffers( 1, &grown );
|
||||
if( grown == 0 ) { return false; }
|
||||
::glBindBuffer( GL_ARRAY_BUFFER, grown );
|
||||
::glBufferData( GL_ARRAY_BUFFER,
|
||||
static_cast<GLsizeiptr>( room ) * sizeof( quantizedmesh::render_vertex ), nullptr, GL_DYNAMIC_DRAW );
|
||||
::glBindBuffer( GL_COPY_READ_BUFFER, m_vertexbuffer );
|
||||
::glBindBuffer( GL_COPY_WRITE_BUFFER, grown );
|
||||
::glCopyBufferSubData( GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0,
|
||||
static_cast<GLsizeiptr>( m_vertexroom ) * sizeof( quantizedmesh::render_vertex ) );
|
||||
::glBindBuffer( GL_COPY_READ_BUFFER, 0 );
|
||||
::glBindBuffer( GL_COPY_WRITE_BUFFER, 0 );
|
||||
::glDeleteBuffers( 1, &m_vertexbuffer );
|
||||
m_vertexbuffer = grown;
|
||||
|
||||
::glBindVertexArray( m_vao );
|
||||
::glBindBuffer( GL_ARRAY_BUFFER, m_vertexbuffer );
|
||||
auto const stride { static_cast<GLsizei>( sizeof( quantizedmesh::render_vertex ) ) };
|
||||
::glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride,
|
||||
reinterpret_cast<void const *>( offsetof( quantizedmesh::render_vertex, x ) ) );
|
||||
::glVertexAttribIPointer( 1, 1, GL_UNSIGNED_SHORT, stride,
|
||||
reinterpret_cast<void const *>( offsetof( quantizedmesh::render_vertex, material ) ) );
|
||||
::glBindVertexArray( 0 );
|
||||
::glBindBuffer( GL_ARRAY_BUFFER, 0 );
|
||||
|
||||
m_freevertices.give( m_vertexroom, room - m_vertexroom );
|
||||
m_vertexroom = room;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
terrain_mesh_arena::grow_indices( std::uint32_t const Wanted ) {
|
||||
|
||||
auto room { m_indexroom };
|
||||
while( ( room < m_indexroom + Wanted ) && ( room < mostindices ) ) { room *= 2; }
|
||||
if( room <= m_indexroom ) { return false; }
|
||||
|
||||
std::uint32_t grown { 0 };
|
||||
::glGenBuffers( 1, &grown );
|
||||
if( grown == 0 ) { return false; }
|
||||
::glBindBuffer( GL_ARRAY_BUFFER, grown );
|
||||
::glBufferData( GL_ARRAY_BUFFER,
|
||||
static_cast<GLsizeiptr>( room ) * sizeof( std::uint32_t ), nullptr, GL_DYNAMIC_DRAW );
|
||||
::glBindBuffer( GL_COPY_READ_BUFFER, m_indexbuffer );
|
||||
::glBindBuffer( GL_COPY_WRITE_BUFFER, grown );
|
||||
::glCopyBufferSubData( GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0,
|
||||
static_cast<GLsizeiptr>( m_indexroom ) * sizeof( std::uint32_t ) );
|
||||
::glBindBuffer( GL_COPY_READ_BUFFER, 0 );
|
||||
::glBindBuffer( GL_COPY_WRITE_BUFFER, 0 );
|
||||
::glDeleteBuffers( 1, &m_indexbuffer );
|
||||
m_indexbuffer = grown;
|
||||
|
||||
::glBindVertexArray( m_vao );
|
||||
::glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, m_indexbuffer );
|
||||
::glBindVertexArray( 0 );
|
||||
::glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
|
||||
|
||||
m_freeindices.give( m_indexroom, room - m_indexroom );
|
||||
m_indexroom = room;
|
||||
return true;
|
||||
}
|
||||
|
||||
terrain_mesh_arena::piece
|
||||
terrain_mesh_arena::put( quantizedmesh::reader::tile_data const &Tile ) {
|
||||
|
||||
piece where;
|
||||
if( ( m_vao == 0 ) || ( true == Tile.vertices.empty() ) || ( true == Tile.indices.empty() ) ) {
|
||||
return where;
|
||||
}
|
||||
auto const vertices { static_cast<std::uint32_t>( Tile.vertices.size() ) };
|
||||
auto const indices { static_cast<std::uint32_t>( Tile.indices.size() ) };
|
||||
|
||||
std::uint32_t atvertex { 0 };
|
||||
if( false == m_freevertices.take( vertices, atvertex ) ) {
|
||||
if( false == grow_vertices( vertices ) ) { return where; }
|
||||
if( false == m_freevertices.take( vertices, atvertex ) ) { return where; }
|
||||
}
|
||||
std::uint32_t atindex { 0 };
|
||||
if( false == m_freeindices.take( indices, atindex ) ) {
|
||||
if( false == grow_indices( indices ) ) {
|
||||
m_freevertices.give( atvertex, vertices );
|
||||
return where;
|
||||
}
|
||||
if( false == m_freeindices.take( indices, atindex ) ) {
|
||||
m_freevertices.give( atvertex, vertices );
|
||||
return where;
|
||||
}
|
||||
}
|
||||
|
||||
::glBindBuffer( GL_ARRAY_BUFFER, m_vertexbuffer );
|
||||
::glBufferSubData( GL_ARRAY_BUFFER,
|
||||
static_cast<GLintptr>( atvertex ) * sizeof( quantizedmesh::render_vertex ),
|
||||
static_cast<GLsizeiptr>( vertices ) * sizeof( quantizedmesh::render_vertex ),
|
||||
Tile.vertices.data() );
|
||||
::glBindBuffer( GL_ARRAY_BUFFER, 0 );
|
||||
|
||||
// the indices are stored as the tile had them, counted from its own first vertex; where the
|
||||
// tile sits in the buffer is given to the draw as its base vertex
|
||||
::glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, m_indexbuffer );
|
||||
::glBufferSubData( GL_ELEMENT_ARRAY_BUFFER,
|
||||
static_cast<GLintptr>( atindex ) * sizeof( std::uint32_t ),
|
||||
static_cast<GLsizeiptr>( indices ) * sizeof( std::uint32_t ),
|
||||
Tile.indices.data() );
|
||||
::glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
|
||||
|
||||
where.firstvertex = atvertex;
|
||||
where.vertices = vertices;
|
||||
where.firstindex = atindex;
|
||||
where.indices = indices;
|
||||
m_verticesheld += vertices;
|
||||
return where;
|
||||
}
|
||||
|
||||
void
|
||||
terrain_mesh_arena::take( piece &Piece ) {
|
||||
|
||||
if( false == Piece.held() ) { return; }
|
||||
m_freevertices.give( Piece.firstvertex, Piece.vertices );
|
||||
m_freeindices.give( Piece.firstindex, Piece.indices );
|
||||
m_verticesheld -= Piece.vertices;
|
||||
Piece = piece {};
|
||||
}
|
||||
87
rendering/terrainmesharena.cppm
Normal file
87
rendering/terrainmesharena.cppm
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
module;
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "scene/quantizedmeshreader.h"
|
||||
|
||||
export module eu07.rendering.terrainmesharena;
|
||||
|
||||
export {
|
||||
|
||||
// One vertex buffer and one index buffer for the whole terrain.
|
||||
//
|
||||
// Tiles are put in and taken out as the camera moves, so each buffer is handed out in pieces and
|
||||
// the pieces are given back. What this buys is the drawing: every resident tile lives in the same
|
||||
// two buffers behind the same vertex array, so a frame is one glMultiDrawElements rather than a
|
||||
// bind and a draw per tile. Vertices are measured from one point for the whole terrain, which is
|
||||
// what makes that possible.
|
||||
//
|
||||
// The buffers grow when a tile will not fit and nothing can be freed; they never shrink.
|
||||
class terrain_mesh_arena {
|
||||
|
||||
public:
|
||||
// where a tile sits in the two buffers
|
||||
struct piece {
|
||||
std::uint32_t firstvertex { 0 };
|
||||
std::uint32_t vertices { 0 };
|
||||
std::uint32_t firstindex { 0 };
|
||||
std::uint32_t indices { 0 };
|
||||
bool held() const { return indices != 0; }
|
||||
};
|
||||
|
||||
terrain_mesh_arena() = default;
|
||||
~terrain_mesh_arena();
|
||||
terrain_mesh_arena( terrain_mesh_arena const & ) = delete;
|
||||
terrain_mesh_arena &operator=( terrain_mesh_arena const & ) = delete;
|
||||
|
||||
bool create();
|
||||
void destroy();
|
||||
|
||||
// puts a tile's mesh in. returns a piece that is not held when it did not fit
|
||||
piece put( quantizedmesh::reader::tile_data const &Tile );
|
||||
void take( piece &Piece );
|
||||
|
||||
// bound for drawing; the index buffer goes with it
|
||||
std::uint32_t vertexarray() const { return m_vao; }
|
||||
|
||||
std::size_t vertexbytes() const { return m_vertexroom * sizeof( quantizedmesh::render_vertex ); }
|
||||
std::size_t indexbytes() const { return m_indexroom * sizeof( std::uint32_t ); }
|
||||
std::size_t bytes() const { return vertexbytes() + indexbytes(); }
|
||||
std::size_t verticesheld() const { return m_verticesheld; }
|
||||
|
||||
private:
|
||||
// a run of free slots in one of the buffers, kept in order so that neighbours can be joined
|
||||
class span_list {
|
||||
public:
|
||||
void clear() { m_free.clear(); }
|
||||
void give( std::uint32_t At, std::uint32_t Count );
|
||||
// the first run large enough, or the count when there is none
|
||||
bool take( std::uint32_t Count, std::uint32_t &At );
|
||||
private:
|
||||
std::map<std::uint32_t, std::uint32_t> m_free; // where to how many
|
||||
};
|
||||
|
||||
bool grow_vertices( std::uint32_t Wanted );
|
||||
bool grow_indices( std::uint32_t Wanted );
|
||||
|
||||
std::uint32_t m_vao { 0 };
|
||||
std::uint32_t m_vertexbuffer { 0 };
|
||||
std::uint32_t m_indexbuffer { 0 };
|
||||
std::uint32_t m_vertexroom { 0 };
|
||||
std::uint32_t m_indexroom { 0 };
|
||||
span_list m_freevertices;
|
||||
span_list m_freeindices;
|
||||
std::size_t m_verticesheld { 0 };
|
||||
};
|
||||
|
||||
} // export
|
||||
@@ -20,76 +20,50 @@ export module eu07.rendering.terrainstatus;
|
||||
|
||||
export {
|
||||
|
||||
// Which level a tile is drawn at follows from what its samples look like on screen: a level is
|
||||
// used out to the distance where one of its sample spacings covers the detail setting in
|
||||
// pixels. Each level doubles the spacing, so each doubles that distance too. Pixelsperunit is
|
||||
// the viewport height over twice the tangent of half the vertical field of view - what a
|
||||
// metre at a metre's distance spans - so zooming in or a taller window pushes detail further.
|
||||
// Which level of the quadtree a tile is drawn at follows from what the coarser mesh would cost in
|
||||
// pixels. Every tile says how far its ground departs from the finest mesh, in metres; that error,
|
||||
// at the tile's distance, covers a number of pixels, and while it covers fewer than the detail
|
||||
// setting asks for there is nothing to gain by going finer. Pixelsperunit is the viewport height
|
||||
// over twice the tangent of half the vertical field of view - what a metre at a metre's distance
|
||||
// spans - so zooming in or a taller window pushes detail further out on its own.
|
||||
//
|
||||
// The finest range never drops below four tile sides, whatever the screen: the morphing below
|
||||
// only closes the seams when a level's range is several tiles long.
|
||||
inline constexpr double terrain_finest_tiles { 4.0 };
|
||||
// This is the format's own rule, and the reason a tile carries its error at all.
|
||||
inline constexpr std::size_t terrain_maxlevels { 16 };
|
||||
|
||||
inline double
|
||||
terrain_finest_range( double const Gridstep, double const Tilesize, double const Pixelsperunit, double const Detail ) {
|
||||
return std::max( terrain_finest_tiles * Tilesize, Gridstep * Pixelsperunit / std::max( 0.1, Detail ) );
|
||||
inline bool
|
||||
terrain_level_enough(
|
||||
double const Error, double const Distance, double const Pixelsperunit, double const Detail ) {
|
||||
return Error * Pixelsperunit <= std::max( 0.1, Detail ) * std::max( 1.0, Distance );
|
||||
}
|
||||
|
||||
// how far the camera may move before the tiles in range and their levels are worked out
|
||||
// again, in tile sides
|
||||
// how far the camera may move before which tiles are wanted is worked out again, as a share of the
|
||||
// finest tile's side
|
||||
inline constexpr double terrain_rescan_tiles { 0.25 };
|
||||
|
||||
// the distance up to which a level is used, given the finest level's range
|
||||
inline double
|
||||
terrain_level_distance( std::uint32_t const Level, double const Finest ) {
|
||||
return Finest * std::pow( 2.0, Level );
|
||||
}
|
||||
|
||||
// A tile morphs into the next level over a band at the far end of its level's range. Two
|
||||
// neighbours at levels L and L+1 then meet exactly when, all along their shared edge, the
|
||||
// finer tile has finished morphing and the coarser one has not started. The band ends a
|
||||
// rescan's worth of travel short of the range, because levels are chosen where the camera
|
||||
// was at the last rescan while the morph follows the camera every frame; and it is a fifth
|
||||
// of the range wide, which keeps the coarser tile's band, a whole range further out, clear of
|
||||
// any edge a finer tile can reach. Both were settled by a geometric test over random camera
|
||||
// positions, three tile sizes and several finest ranges (tools/terraincook/seamcheck.py),
|
||||
// which finds no gap with these values and finds them at every level boundary with a band
|
||||
// ending at the range itself.
|
||||
inline constexpr double terrain_morph_band { 0.2 };
|
||||
|
||||
// the distances, from the camera, over which a level L tile morphs into level L+1
|
||||
inline double
|
||||
terrain_morph_end( std::uint32_t const Level, double const Finest, double const Tilesize ) {
|
||||
return terrain_level_distance( Level, Finest ) - terrain_rescan_tiles * Tilesize;
|
||||
}
|
||||
|
||||
inline double
|
||||
terrain_morph_begin( std::uint32_t const Level, double const Finest, double const Tilesize ) {
|
||||
return terrain_morph_end( Level, Finest, Tilesize ) - terrain_morph_band * terrain_level_distance( Level, Finest );
|
||||
}
|
||||
|
||||
// what one terrain field's streamer is doing, kept by the field and read by the debug panel
|
||||
// what one terrain's streamer is doing, kept by it and read by the debug panel
|
||||
struct terrain_statistics {
|
||||
std::size_t resident { 0 }; // tiles holding gl textures
|
||||
std::size_t inview { 0 }; // resident tiles within range in the last update
|
||||
std::size_t resident { 0 }; // tiles held in the vertex and index buffers
|
||||
std::size_t chosen { 0 }; // tiles the last walk of the quadtree settled on drawing
|
||||
std::size_t drawn { 0 }; // of those, the ones inside the frustum in the last draw
|
||||
std::size_t wanted { 0 }; // tiles in range missing, or at the wrong level
|
||||
std::size_t uploaded { 0 }; // uploads since opening
|
||||
std::size_t dropped { 0 }; // arrivals discarded as stale since opening
|
||||
std::size_t texturebytes { 0 };
|
||||
std::array<std::size_t, 8> perlevel {}; // tiles in view at each level
|
||||
std::size_t triangles { 0 }; // triangles in the last draw
|
||||
std::size_t wanted { 0 }; // tiles the walk asked for and did not have
|
||||
std::size_t uploaded { 0 }; // tiles put in the buffers since opening
|
||||
std::size_t dropped { 0 }; // arrivals discarded as no longer wanted
|
||||
std::size_t gpubytes { 0 }; // what the two buffers hold
|
||||
std::array<std::size_t, terrain_maxlevels> perlevel {}; // tiles drawn at each level
|
||||
};
|
||||
|
||||
// one field as the debug panel shows it. the renderer writes it once a frame from the main
|
||||
// view, the panel reads it; neither needs to know the other's types
|
||||
// one terrain as the debug panel shows it. the renderer writes it once a frame from the main view,
|
||||
// the panel reads it; neither needs to know the other's types
|
||||
struct terrain_field_status {
|
||||
std::string path;
|
||||
float gridstep { 0.f };
|
||||
float tilesize { 0.f };
|
||||
float tilesize { 0.f }; // side of a tile at the finest level, in metres
|
||||
float range { 0.f };
|
||||
float finest { 0.f }; // range of the finest level, from the screen
|
||||
float detail { 0.f }; // pixels an error may cover before a finer level is used
|
||||
std::uint32_t levels { 0 };
|
||||
std::size_t backlog { 0 }; // tiles the loader has queued or finished and not handed over
|
||||
std::array<float, terrain_maxlevels> leveltile {}; // tile side at each level
|
||||
std::array<float, terrain_maxlevels> levelerror {}; // metres a level departs from the finest
|
||||
terrain_statistics stats;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,10 +25,14 @@ terrain_tile_loader::~terrain_tile_loader() {
|
||||
}
|
||||
|
||||
bool
|
||||
terrain_tile_loader::open( std::string const &Path ) {
|
||||
terrain_tile_loader::open(
|
||||
std::string const &Path, double const Originx, double const Originy, double const Originz ) {
|
||||
|
||||
close();
|
||||
if( false == m_reader.open( Path ) ) { return false; }
|
||||
m_originx = Originx;
|
||||
m_originy = Originy;
|
||||
m_originz = Originz;
|
||||
m_stop = false;
|
||||
m_thread = std::thread( &terrain_tile_loader::work, this );
|
||||
return true;
|
||||
@@ -53,24 +57,22 @@ terrain_tile_loader::close() {
|
||||
}
|
||||
|
||||
bool
|
||||
terrain_tile_loader::underway( request const &Tile ) const {
|
||||
terrain_tile_loader::underway( quantizedmesh::tile_address const &Tile ) const {
|
||||
|
||||
if( ( m_running.has_value() ) && ( *m_running == Tile ) ) { return true; }
|
||||
return std::any_of(
|
||||
m_done.begin(), m_done.end(),
|
||||
[ &Tile ]( payload const &Done ) { return Done.tile == Tile; } );
|
||||
[ &Tile ]( payload const &Done ) { return Done.data.tile == Tile; } );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_tile_loader::want( std::vector<request> const &Requests ) {
|
||||
terrain_tile_loader::want( std::vector<quantizedmesh::tile_address> const &Requests ) {
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
m_pending.clear();
|
||||
for( auto const &tile : Requests ) {
|
||||
if( false == underway( tile ) ) {
|
||||
m_pending.push_back( tile );
|
||||
}
|
||||
if( false == underway( tile ) ) { m_pending.push_back( tile ); }
|
||||
}
|
||||
}
|
||||
m_wake.notify_one();
|
||||
@@ -105,7 +107,7 @@ void
|
||||
terrain_tile_loader::work() {
|
||||
|
||||
while( true ) {
|
||||
request tile;
|
||||
quantizedmesh::tile_address tile;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock( m_mutex );
|
||||
m_wake.wait( lock, [ this ]() {
|
||||
@@ -116,40 +118,11 @@ terrain_tile_loader::work() {
|
||||
m_running = tile;
|
||||
}
|
||||
|
||||
// the read itself happens outside the lock, so the render thread can replace the
|
||||
// wish list while a tile is being decoded
|
||||
// the read itself happens outside the lock, so the render thread can replace the wish
|
||||
// list while a tile is being decoded
|
||||
payload result;
|
||||
result.tile = tile;
|
||||
|
||||
quantizedmesh::reader::tile_data data;
|
||||
result.valid = m_reader.read_tile( tile.x, tile.z, tile.level, data );
|
||||
|
||||
if( result.valid ) {
|
||||
// Copy data to payload
|
||||
result.positions_x = std::move( data.positions_x );
|
||||
result.positions_y = std::move( data.positions_y );
|
||||
result.positions_z = std::move( data.positions_z );
|
||||
result.uvs_u = std::move( data.uvs_u );
|
||||
result.uvs_v = std::move( data.uvs_v );
|
||||
result.materials = std::move( data.materials );
|
||||
result.indices = std::move( data.indices );
|
||||
|
||||
result.north_edge = std::move( data.north_edge );
|
||||
result.south_edge = std::move( data.south_edge );
|
||||
result.west_edge = std::move( data.west_edge );
|
||||
result.east_edge = std::move( data.east_edge );
|
||||
|
||||
result.center_x = data.center_x;
|
||||
result.center_y = data.center_y;
|
||||
result.center_z = data.center_z;
|
||||
result.min_x = data.min_x;
|
||||
result.min_y = data.min_y;
|
||||
result.min_z = data.min_z;
|
||||
result.max_x = data.max_x;
|
||||
result.max_y = data.max_y;
|
||||
result.max_z = data.max_z;
|
||||
result.geometric_error = data.geometric_error;
|
||||
}
|
||||
result.data.tile = tile;
|
||||
result.valid = m_reader.read_tile( tile, result.data, m_originx, m_originy, m_originz );
|
||||
|
||||
std::lock_guard<std::mutex> lock( m_mutex );
|
||||
m_running.reset();
|
||||
|
||||
@@ -23,49 +23,18 @@ export module eu07.rendering.terraintileloader;
|
||||
|
||||
export {
|
||||
|
||||
// Reads and decodes TIN (Quantized Mesh) terrain tiles on a thread of its own.
|
||||
// Reads and decodes terrain mesh tiles on a thread of its own.
|
||||
//
|
||||
// Reads .qm tile files, dequantizes vertices, and hands back ready-to-upload vertex/index
|
||||
// data. Does the I/O and decompression off the render thread. The renderer uploads at its own
|
||||
// pace, and the loader prioritizes nearest tiles first.
|
||||
//
|
||||
// Replaces heightfield tile loading entirely - TIN is the only terrain format now.
|
||||
// Reading a tile means finding it in the archive, unpacking it and turning the quantized vertices
|
||||
// back into something a vertex buffer will take. None of that belongs on the render thread. The
|
||||
// renderer says which tiles it wants, nearest first, and collects whatever has arrived at its own
|
||||
// pace.
|
||||
class terrain_tile_loader {
|
||||
|
||||
public:
|
||||
struct request {
|
||||
std::int32_t x { 0 }, z { 0 };
|
||||
std::uint32_t level { 0 };
|
||||
|
||||
bool operator==( request const & ) const = default;
|
||||
};
|
||||
|
||||
struct payload {
|
||||
request tile;
|
||||
quantizedmesh::reader::tile_data data;
|
||||
bool valid { false };
|
||||
|
||||
// Vertex data (dequantized, ready for GPU)
|
||||
std::vector<double> positions_x;
|
||||
std::vector<double> positions_y;
|
||||
std::vector<double> positions_z;
|
||||
std::vector<float> uvs_u;
|
||||
std::vector<float> uvs_v;
|
||||
std::vector<std::uint8_t> materials;
|
||||
|
||||
// Index data
|
||||
std::vector<std::uint32_t> indices;
|
||||
|
||||
// Edge indices for seamless LOD stitching
|
||||
std::vector<std::uint16_t> north_edge;
|
||||
std::vector<std::uint16_t> south_edge;
|
||||
std::vector<std::uint16_t> west_edge;
|
||||
std::vector<std::uint16_t> east_edge;
|
||||
|
||||
// Tile bounds (for placement)
|
||||
double center_x, center_y, center_z;
|
||||
double min_x, min_y, min_z;
|
||||
double max_x, max_y, max_z;
|
||||
double geometric_error;
|
||||
};
|
||||
|
||||
terrain_tile_loader() = default;
|
||||
@@ -73,14 +42,15 @@ public:
|
||||
terrain_tile_loader( terrain_tile_loader const & ) = delete;
|
||||
terrain_tile_loader &operator=( terrain_tile_loader const & ) = delete;
|
||||
|
||||
// opens the file on the loader's own handle and starts the thread
|
||||
bool open( std::string const &Path );
|
||||
// opens the source on the loader's own handle and starts the thread. Origin is what the
|
||||
// vertices come back measured from
|
||||
bool open( std::string const &Path, double Originx, double Originy, double Originz );
|
||||
// stops the thread; anything not collected is dropped
|
||||
void close();
|
||||
|
||||
// replaces the list of wanted tiles, nearest first. tiles being read or already read and
|
||||
// not yet collected are left out of it, so asking again every frame costs nothing
|
||||
void want( std::vector<request> const &Requests );
|
||||
// replaces the list of wanted tiles, nearest first. tiles being read or already read and not
|
||||
// yet collected are left out of it, so asking again every frame costs nothing
|
||||
void want( std::vector<quantizedmesh::tile_address> const &Requests );
|
||||
// moves up to Count finished tiles into Out, returning how many
|
||||
std::size_t collect( std::vector<payload> &Out, std::size_t const Count );
|
||||
// tiles queued or finished and not yet collected
|
||||
@@ -88,20 +58,21 @@ public:
|
||||
|
||||
private:
|
||||
void work();
|
||||
// whether the tile is being read or waits to be collected. caller holds the lock
|
||||
bool underway( request const &Tile ) const;
|
||||
// whether the tile is being read or waits to be collected. the caller holds the lock
|
||||
bool underway( quantizedmesh::tile_address const &Tile ) const;
|
||||
|
||||
quantizedmesh::reader m_reader;
|
||||
double m_originx { 0.0 }, m_originy { 0.0 }, m_originz { 0.0 };
|
||||
std::thread m_thread;
|
||||
mutable std::mutex m_mutex;
|
||||
std::condition_variable m_wake;
|
||||
std::deque<request> m_pending;
|
||||
std::optional<request> m_running;
|
||||
std::deque<quantizedmesh::tile_address> m_pending;
|
||||
std::optional<quantizedmesh::tile_address> m_running;
|
||||
std::deque<payload> m_done;
|
||||
bool m_stop { false };
|
||||
// finished tiles allowed to wait for collection. past this the thread stops reading:
|
||||
// the renderer uploads at its own pace, and a backlog read for where the camera was a
|
||||
// second ago is work spent on tiles nobody wants any more
|
||||
// finished tiles allowed to wait for collection. past this the thread stops reading: the
|
||||
// renderer uploads at its own pace, and a backlog read for where the camera was a second ago
|
||||
// is work spent on tiles nobody wants any more
|
||||
static constexpr std::size_t donelimit { 32 };
|
||||
};
|
||||
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
module;
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <glad/glad.h>
|
||||
|
||||
module eu07.rendering.terraintilepool_tin;
|
||||
import eu07.glm;
|
||||
|
||||
terrain_tile_pool_tin::terrain_tile_pool_tin() = default;
|
||||
|
||||
terrain_tile_pool_tin::~terrain_tile_pool_tin() = default;
|
||||
|
||||
terrain_tile_pool_tin::tile_handle
|
||||
terrain_tile_pool_tin::upload(
|
||||
std::vector<double> const &positions_x,
|
||||
std::vector<double> const &positions_y,
|
||||
std::vector<double> const &positions_z,
|
||||
std::vector<float> const &uvs_u,
|
||||
std::vector<float> const &uvs_v,
|
||||
std::vector<std::uint8_t> const &materials,
|
||||
std::vector<std::uint32_t> const &indices,
|
||||
glm::dvec3 const &origin
|
||||
) {
|
||||
tile_handle handle;
|
||||
handle.origin = origin;
|
||||
|
||||
if( positions_x.empty() || indices.empty() ) {
|
||||
return handle; // Invalid
|
||||
}
|
||||
|
||||
std::size_t const vertex_count = positions_x.size();
|
||||
handle.vertex_count = static_cast<std::uint32_t>( vertex_count );
|
||||
handle.index_count = static_cast<std::uint32_t>( indices.size() );
|
||||
|
||||
// Pack vertices into interleaved format
|
||||
std::vector<vertex> vertices;
|
||||
vertices.reserve( vertex_count );
|
||||
|
||||
for( std::size_t i = 0; i < vertex_count; ++i ) {
|
||||
vertex v;
|
||||
// Store relative to origin for camera-relative rendering
|
||||
v.x = static_cast<float>( positions_x[ i ] - origin.x );
|
||||
v.y = static_cast<float>( positions_y[ i ] - origin.y );
|
||||
v.z = static_cast<float>( positions_z[ i ] - origin.z );
|
||||
v.u = uvs_u[ i ];
|
||||
v.v = uvs_v[ i ];
|
||||
v.material = i < materials.size() ? materials[ i ] : 0;
|
||||
v.pad[0] = v.pad[1] = v.pad[2] = 0;
|
||||
vertices.push_back( v );
|
||||
}
|
||||
|
||||
// Create VAO
|
||||
glGenVertexArrays( 1, &handle.vao_id );
|
||||
glBindVertexArray( handle.vao_id );
|
||||
|
||||
// Create and upload VBO
|
||||
glGenBuffers( 1, &handle.vbo_id );
|
||||
glBindBuffer( GL_ARRAY_BUFFER, handle.vbo_id );
|
||||
glBufferData( GL_ARRAY_BUFFER,
|
||||
vertices.size() * sizeof( vertex ),
|
||||
vertices.data(),
|
||||
GL_STATIC_DRAW );
|
||||
|
||||
// Position attribute (location 0)
|
||||
glEnableVertexAttribArray( 0 );
|
||||
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, sizeof( vertex ),
|
||||
reinterpret_cast<void *>( offsetof( vertex, x ) ) );
|
||||
|
||||
// UV attribute (location 1)
|
||||
glEnableVertexAttribArray( 1 );
|
||||
glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, sizeof( vertex ),
|
||||
reinterpret_cast<void *>( offsetof( vertex, u ) ) );
|
||||
|
||||
// Material attribute (location 2)
|
||||
glEnableVertexAttribArray( 2 );
|
||||
glVertexAttribIPointer( 2, 1, GL_UNSIGNED_BYTE, sizeof( vertex ),
|
||||
reinterpret_cast<void *>( offsetof( vertex, material ) ) );
|
||||
|
||||
// Create and upload IBO
|
||||
glGenBuffers( 1, &handle.ibo_id );
|
||||
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, handle.ibo_id );
|
||||
glBufferData( GL_ELEMENT_ARRAY_BUFFER,
|
||||
indices.size() * sizeof( std::uint32_t ),
|
||||
indices.data(),
|
||||
GL_STATIC_DRAW );
|
||||
|
||||
// Cleanup
|
||||
glBindVertexArray( 0 );
|
||||
glBindBuffer( GL_ARRAY_BUFFER, 0 );
|
||||
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
|
||||
|
||||
// Track allocation
|
||||
m_allocated_vertices += vertex_count;
|
||||
m_allocated_indices += indices.size();
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
void
|
||||
terrain_tile_pool_tin::free( tile_handle &Handle ) {
|
||||
if( !Handle.valid() ) return;
|
||||
|
||||
// Free GL resources
|
||||
if( Handle.vao_id != 0 ) {
|
||||
glDeleteVertexArrays( 1, &Handle.vao_id );
|
||||
}
|
||||
if( Handle.vbo_id != 0 ) {
|
||||
glDeleteBuffers( 1, &Handle.vbo_id );
|
||||
}
|
||||
if( Handle.ibo_id != 0 ) {
|
||||
glDeleteBuffers( 1, &Handle.ibo_id );
|
||||
}
|
||||
|
||||
// Update stats
|
||||
m_allocated_vertices -= Handle.vertex_count;
|
||||
m_allocated_indices -= Handle.index_count;
|
||||
|
||||
// Invalidate handle
|
||||
Handle = tile_handle{};
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
module;
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
export module eu07.rendering.terraintilepool_tin;
|
||||
import eu07.gl.buffer;
|
||||
import eu07.glm;
|
||||
|
||||
export {
|
||||
|
||||
// GPU storage for TIN terrain tiles - vertex and index buffers.
|
||||
// Replaces texture array approach used by heightfield.
|
||||
//
|
||||
// Each tile gets its own VBO/IBO pair. Tiles are uploaded as they arrive from the loader
|
||||
// and freed when they leave range. Budget is vertex/index count rather than texture memory.
|
||||
class terrain_tile_pool_tin {
|
||||
|
||||
public:
|
||||
// Vertex format for terrain
|
||||
struct vertex {
|
||||
float x, y, z; // Position (relative to tile origin)
|
||||
float u, v; // Texture coordinates
|
||||
std::uint8_t material; // Material index
|
||||
std::uint8_t pad[3]; // Alignment to 24 bytes
|
||||
};
|
||||
|
||||
static_assert( sizeof( vertex ) == 24 );
|
||||
|
||||
// Handle to uploaded tile
|
||||
struct tile_handle {
|
||||
std::uint32_t vbo_id { 0 };
|
||||
std::uint32_t ibo_id { 0 };
|
||||
std::uint32_t vao_id { 0 };
|
||||
std::uint32_t vertex_count { 0 };
|
||||
std::uint32_t index_count { 0 };
|
||||
glm::dvec3 origin { 0.0 }; // World origin for camera-relative rendering
|
||||
|
||||
bool valid() const { return vbo_id != 0; }
|
||||
};
|
||||
|
||||
terrain_tile_pool_tin();
|
||||
~terrain_tile_pool_tin();
|
||||
|
||||
terrain_tile_pool_tin( terrain_tile_pool_tin const & ) = delete;
|
||||
terrain_tile_pool_tin &operator=( terrain_tile_pool_tin const & ) = delete;
|
||||
|
||||
// Upload tile data to GPU
|
||||
tile_handle upload(
|
||||
std::vector<double> const &positions_x,
|
||||
std::vector<double> const &positions_y,
|
||||
std::vector<double> const &positions_z,
|
||||
std::vector<float> const &uvs_u,
|
||||
std::vector<float> const &uvs_v,
|
||||
std::vector<std::uint8_t> const &materials,
|
||||
std::vector<std::uint32_t> const &indices,
|
||||
glm::dvec3 const &origin // Tile center for relative positioning
|
||||
);
|
||||
|
||||
// Free tile from GPU
|
||||
void free( tile_handle &Handle );
|
||||
|
||||
// Statistics
|
||||
std::size_t allocated_vertices() const { return m_allocated_vertices; }
|
||||
std::size_t allocated_indices() const { return m_allocated_indices; }
|
||||
std::size_t allocated_bytes() const {
|
||||
return m_allocated_vertices * sizeof( vertex ) + m_allocated_indices * sizeof( std::uint32_t );
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t m_allocated_vertices { 0 };
|
||||
std::size_t m_allocated_indices { 0 };
|
||||
};
|
||||
|
||||
} // export
|
||||
Reference in New Issue
Block a user