mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-30 06:10:28 +01:00
Give the terrain its own archive, its normals, and a level per tile
The tiles moved out of the scenery's pak into an archive of their own: a sorted index of fixed-size entries found by where a tile sits in the quadtree rather than by a name, each entry compressed on its own so a tile can be read without touching the rest. A name apiece would have been seventy megabytes of index to read before the first tile could be drawn. An entry carries what its tile's ground asks for - how far it departs from the finest mesh, and how long its triangles are - because which level to draw has to be settled for tiles that are not on the card yet, and the index is the only thing about them that is read up front. Until now the figure came from the tile's level, which is the worst tile in it, so one steep corner spoke for every tile that shared its level. The mesh carries a normal per vertex, octahedron-encoded into two bytes, in place of the face normal the fragment stage worked out from how the view position changed across a triangle. A face normal makes the ground read as facets, and a coarser level's facets face differently from the ones they stand for, so the light over a hillside changed as the level did. A geometry stage hands the fragment stage the three materials of the triangle it is in and the barycentric weights, so ground changes over a triangle rather than along its edge; 3.3 has no gl_BaryCoordEXT. And the level a tile is drawn at now counts the camera's height. The distance was measured on the ground plane alone, which puts the tile underfoot nought metres away from a camera a kilometre above it - and nought metres asks for the finest level there is, so a view from the air wanted the whole ground at full detail, more tiles than the buffers hold, and what would not fit came out as holes. Archives written under the old rules are refused at the door and baked again.
This commit is contained in:
@@ -2917,9 +2917,12 @@ void opengl33_renderer::Render_terrain()
|
||||
Global.TerrainDetail);
|
||||
field->update(viewpoint);
|
||||
}
|
||||
field->render(viewpoint, [this](glm::dvec3 const &Centre, float const Radius) {
|
||||
return m_renderpass.pass_camera.visible(scene::bounding_area(Centre, Radius));
|
||||
});
|
||||
field->render(
|
||||
viewpoint,
|
||||
[this](glm::dvec3 const &Centre, float const Radius) {
|
||||
return m_renderpass.pass_camera.visible(scene::bounding_area(Centre, Radius));
|
||||
},
|
||||
m_current_viewport->main);
|
||||
}
|
||||
|
||||
if (m_current_viewport->main)
|
||||
@@ -2938,10 +2941,14 @@ void opengl33_renderer::Render_terrain()
|
||||
{
|
||||
status.leveltile[level] = field.leveltile(level);
|
||||
status.levelerror[level] = field.levelerror(level);
|
||||
status.leveledge[level] = field.leveledge(level);
|
||||
}
|
||||
}
|
||||
status.range = field.range();
|
||||
status.detail = field.detailpixels();
|
||||
status.pixelsperunit =
|
||||
static_cast<float>(m_current_viewport->height) /
|
||||
(2.f * std::tan(glm::radians(Global.FieldOfView / Global.ZoomFactor) * 0.5f));
|
||||
status.backlog = field.backlog();
|
||||
status.stats = field.stats();
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ terrain_clipmap::open( std::string const &Path ) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if( false == m_arena.create() ) {
|
||||
if( ( false == m_arena.create( false ) ) || ( false == m_widearena.create( true ) ) ) {
|
||||
ErrorLog( "Terrain: cannot allocate the terrain buffers" );
|
||||
close();
|
||||
return false;
|
||||
@@ -65,8 +65,10 @@ terrain_clipmap::open( std::string const &Path ) {
|
||||
|
||||
try {
|
||||
gl::shader vertex( "terrainmesh.vert" );
|
||||
gl::shader geometry( "terrainmesh.geom" );
|
||||
gl::shader fragment( "terrainmesh.frag" );
|
||||
m_shader.emplace( std::vector<std::reference_wrapper<gl::shader const>>( { vertex, fragment } ) );
|
||||
m_shader.emplace(
|
||||
std::vector<std::reference_wrapper<gl::shader const>>( { vertex, geometry, fragment } ) );
|
||||
}
|
||||
catch( gl::shader_exception const &error ) {
|
||||
ErrorLog( std::string( "Terrain: shader failed, " ) + error.what() );
|
||||
@@ -96,7 +98,7 @@ terrain_clipmap::open( std::string const &Path ) {
|
||||
"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_reader.tilecount() ) + " tiles, "
|
||||
+ std::to_string( m_table.materials.size() ) + " ground materials" );
|
||||
return true;
|
||||
}
|
||||
@@ -124,8 +126,9 @@ terrain_clipmap::close() {
|
||||
m_wanted.clear();
|
||||
m_arrived.clear();
|
||||
m_asked.clear();
|
||||
m_readiness.clear();
|
||||
m_maydescend.clear();
|
||||
m_arena.destroy();
|
||||
m_widearena.destroy();
|
||||
m_ground.destroy();
|
||||
m_shader.reset();
|
||||
m_reader.close();
|
||||
@@ -149,6 +152,12 @@ terrain_clipmap::levelerror( std::uint32_t const Level ) const {
|
||||
return m_table.error( Level );
|
||||
}
|
||||
|
||||
float
|
||||
terrain_clipmap::leveledge( std::uint32_t const Level ) const {
|
||||
|
||||
return m_table.edge( Level );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::detail( double const Pixelsperunit, double const Pixels ) {
|
||||
|
||||
@@ -164,8 +173,15 @@ terrain_clipmap::square( quantizedmesh::tile_address const &Tile, double &West,
|
||||
North = m_table.north + Tile.z * Side;
|
||||
}
|
||||
|
||||
// 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
|
||||
// The distance to the nearest point of the tile's box, 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.
|
||||
//
|
||||
// The height counts as much as the other two. Measuring on the ground plane alone makes the tile
|
||||
// underfoot nought metres away from a camera a kilometre above it, and nought metres asks for the
|
||||
// finest level there is - so a view from the air drew the whole ground at full detail, which is
|
||||
// more tiles than the buffers hold, and what would not fit came out as holes. The tile's own height
|
||||
// range is not known until it is read, so the terrain's is used for all of them: too near rather
|
||||
// than too far, which errs towards detail.
|
||||
double
|
||||
terrain_clipmap::distance_to( quantizedmesh::tile_address const &Tile, glm::dvec3 const &Viewpoint ) const {
|
||||
|
||||
@@ -173,35 +189,47 @@ terrain_clipmap::distance_to( quantizedmesh::tile_address const &Tile, glm::dvec
|
||||
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 } );
|
||||
auto const nearesty { std::clamp( Viewpoint.y, m_table.lowest, m_table.highest ) };
|
||||
return glm::length( glm::dvec3 {
|
||||
nearestx - Viewpoint.x, nearesty - Viewpoint.y, nearestz - Viewpoint.z } );
|
||||
}
|
||||
|
||||
bool
|
||||
terrain_clipmap::present( quantizedmesh::tile_address const &Tile ) const {
|
||||
|
||||
return m_reader.present().contains( quantizedmesh::tile_key( Tile ) );
|
||||
return m_reader.contains( Tile );
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// What is asked is the tile's own error and its own triangles, out of the archive's index, not the
|
||||
// worst in its level. The two are far apart: a level's figure is set by whichever tile in it holds
|
||||
// the steepest ground, and with that one figure standing for all of them a flat field twenty
|
||||
// kilometres out was split as finely as the valley wall that set it.
|
||||
bool
|
||||
terrain_clipmap::good_enough( quantizedmesh::tile_address const &Tile, double const Distance ) const {
|
||||
|
||||
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 );
|
||||
auto const measure { m_reader.measure( Tile ) };
|
||||
return terrain_level_enough(
|
||||
measure.error, measure.edge, 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.
|
||||
// card - every tile on the way down, not only the deepest. 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. And a level fetched only once its children turn out
|
||||
// to be late is fetched too late to cover anything.
|
||||
//
|
||||
// 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.
|
||||
// Returns whether this tile's ground can be drawn at all: either everything below it is there, or the
|
||||
// tile itself is, and then it stands in for the lot. That distinction is the whole point. Answering
|
||||
// only "is the subtree complete" makes one missing tile twenty kilometres away answer no for its
|
||||
// parent, and so for its parent's parent, all the way to the root - and then the entire terrain is
|
||||
// drawn as the root tile until it arrives. Which is what flickering while the camera moved was.
|
||||
bool
|
||||
terrain_clipmap::need( quantizedmesh::tile_address const &Tile, glm::dvec3 const &Viewpoint ) {
|
||||
|
||||
@@ -212,23 +240,22 @@ terrain_clipmap::need( quantizedmesh::tile_address const &Tile, glm::dvec3 const
|
||||
|
||||
auto const key { quantizedmesh::tile_key( Tile ) };
|
||||
auto const resident { m_tiles.contains( key ) };
|
||||
if( false == resident ) { ask_for( Tile, distance ); }
|
||||
|
||||
if( true == good_enough( Tile, distance ) ) {
|
||||
if( false == resident ) { ask_for( Tile, distance ); }
|
||||
m_readiness.emplace( key, resident );
|
||||
m_maydescend.emplace( key, false );
|
||||
return resident;
|
||||
}
|
||||
|
||||
auto ready { true };
|
||||
auto descend { 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;
|
||||
descend = need( { Tile.level + 1, Tile.x * 2 + stepx, Tile.z * 2 + stepz }, Viewpoint ) && descend;
|
||||
}
|
||||
}
|
||||
if( ( false == ready ) && ( false == resident ) ) { ask_for( Tile, distance ); }
|
||||
m_readiness.emplace( key, ready );
|
||||
return ready;
|
||||
m_maydescend.emplace( key, descend );
|
||||
return descend || resident;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -240,10 +267,12 @@ terrain_clipmap::choose( quantizedmesh::tile_address const &Tile, glm::dvec3 con
|
||||
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 )
|
||||
auto const descend { m_maydescend.find( key ) };
|
||||
if( ( descend != m_maydescend.end() ) && ( true == descend->second )
|
||||
&& ( false == good_enough( Tile, distance ) ) ) {
|
||||
// the whole cut under this tile is on the card: draw that instead
|
||||
// the whole cut under this tile is on the card: draw that instead, and hold on to this one,
|
||||
// which is what covers this ground again as soon as the camera moves
|
||||
m_kept.push_back( key );
|
||||
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 );
|
||||
@@ -271,21 +300,40 @@ terrain_clipmap::walk( glm::dvec3 const &Viewpoint ) {
|
||||
m_walkdetail = m_detail;
|
||||
m_walked = true;
|
||||
m_chosen.clear();
|
||||
m_kept.clear();
|
||||
m_wishes.clear();
|
||||
m_asked.clear();
|
||||
m_readiness.clear();
|
||||
m_maydescend.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
|
||||
// Coarsest first, and within a level nearest first. What covers ground has to arrive before what
|
||||
// refines it: a fine tile read ahead of the level standing in for it leaves a hole for as long as
|
||||
// the queue takes, and the ground under the camera is still first among equals.
|
||||
std::sort( m_wishes.begin(), m_wishes.end(),
|
||||
[]( auto const &Left, auto const &Right ) { return Left.first < Right.first; } );
|
||||
[]( auto const &Left, auto const &Right ) {
|
||||
if( Left.second.level != Right.second.level ) { return Left.second.level < Right.second.level; }
|
||||
return Left.first < Right.first; } );
|
||||
m_wanted.clear();
|
||||
m_wanted.reserve( m_wishes.size() );
|
||||
for( auto const &wish : m_wishes ) { m_wanted.push_back( wish.second ); }
|
||||
m_loader.want( m_wanted );
|
||||
|
||||
// A walk that settles on a handful of tiles where the one before had a hundred is the ground going
|
||||
// coarse for a frame: somewhere a tile is missing and every level between it and the root is
|
||||
// missing too, so the root is the only thing left to stand in. Rare, and worth knowing about, so it
|
||||
// says which tiles it was waiting for.
|
||||
if( ( m_stats.chosen > 8 ) && ( m_chosen.size() * 4 < m_stats.chosen ) && ( false == m_wishes.empty() ) ) {
|
||||
std::string waiting;
|
||||
for( std::size_t index = 0; ( index < m_wishes.size() ) && ( index < 6 ); ++index ) {
|
||||
auto const &tile { m_wishes[ index ].second };
|
||||
waiting += " " + std::to_string( tile.level ) + "/" + std::to_string( tile.x ) + "/"
|
||||
+ std::to_string( tile.z ) + " at " + std::to_string( static_cast<int>( m_wishes[ index ].first ) ) + " m";
|
||||
}
|
||||
WriteLog( "Terrain: the walk fell from " + std::to_string( m_stats.chosen ) + " tiles to "
|
||||
+ std::to_string( m_chosen.size() ) + ", waiting for" + waiting );
|
||||
}
|
||||
m_stats.chosen = m_chosen.size();
|
||||
m_stats.wanted = m_wanted.size();
|
||||
}
|
||||
@@ -301,11 +349,14 @@ terrain_clipmap::put_in( terrain_tile_loader::payload const &Tile ) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto piece { m_arena.put( Tile.data ) };
|
||||
// the narrow arena first; only what will not fit in it costs four bytes an index
|
||||
auto const wide { Tile.data.vertices.size() > m_arena.vertexlimit() };
|
||||
auto piece { wide ? m_widearena.put( Tile.data ) : m_arena.put( Tile.data ) };
|
||||
if( false == piece.held() ) { return; }
|
||||
|
||||
resident_tile tile;
|
||||
tile.piece = piece;
|
||||
tile.wide = wide;
|
||||
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;
|
||||
@@ -321,7 +372,12 @@ terrain_clipmap::put_in( terrain_tile_loader::payload const &Tile ) {
|
||||
void
|
||||
terrain_clipmap::retire_unused() {
|
||||
|
||||
if( m_arena.bytes() <= m_budget ) { return; }
|
||||
// Measured against what the tiles take up, not against what the buffers were allocated at. The
|
||||
// allocation only ever grows - gl cannot hand a buffer back in pieces - so gating on it means
|
||||
// nothing is let go until the buffers reach the cap, and from then on everything is let go every
|
||||
// frame and read again. That is how three thousand tiles came to be held for a working set of
|
||||
// forty, and why it stopped exactly at the cap.
|
||||
if( m_arena.usedbytes() + m_widearena.usedbytes() <= m_budget ) { return; }
|
||||
|
||||
std::vector<std::pair<std::uint64_t, std::int64_t>> aged;
|
||||
aged.reserve( m_tiles.size() );
|
||||
@@ -331,13 +387,23 @@ terrain_clipmap::retire_unused() {
|
||||
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 ) };
|
||||
// down to well under the cap rather than just under it, so that this settles instead of running
|
||||
// every frame
|
||||
auto const target { m_budget * 3 / 4 };
|
||||
std::size_t letgo { 0 };
|
||||
for( auto const &[ when, key ] : aged ) {
|
||||
if( m_arena.usedbytes() + m_widearena.usedbytes() <= target ) { break; }
|
||||
auto const found { m_tiles.find( key ) };
|
||||
if( found == m_tiles.end() ) { continue; }
|
||||
m_arena.take( found->second.piece );
|
||||
if( true == found->second.wide ) { m_widearena.take( found->second.piece ); }
|
||||
else { m_arena.take( found->second.piece ); }
|
||||
m_tiles.erase( found );
|
||||
++letgo;
|
||||
}
|
||||
if( letgo > 0 ) {
|
||||
WriteLog( "Terrain: over budget, let go of " + std::to_string( letgo ) + " tiles, now holding "
|
||||
+ std::to_string( ( m_arena.usedbytes() + m_widearena.usedbytes() ) / 1048576 ) + " MB in "
|
||||
+ std::to_string( ( m_arena.roombytes() + m_widearena.roombytes() ) / 1048576 ) + " MB of buffers" );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,18 +434,22 @@ terrain_clipmap::update( glm::dvec3 const &Viewpoint ) {
|
||||
walk( Viewpoint );
|
||||
}
|
||||
|
||||
// what the walk settled on is wanted now, and is what eviction spares
|
||||
m_stats.perlevel.fill( 0 );
|
||||
// what the walk settled on is wanted now, and is what eviction spares. how many of them end up
|
||||
// drawn at each level is counted by the draw itself, which is the one that knows
|
||||
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 ) ];
|
||||
}
|
||||
// the levels above what is drawn are wanted too, as the cover for wherever the camera turns next
|
||||
for( auto const key : m_kept ) {
|
||||
auto const found { m_tiles.find( key ) };
|
||||
if( found != m_tiles.end() ) { found->second.lastused = m_frame; }
|
||||
}
|
||||
|
||||
retire_unused();
|
||||
m_stats.resident = m_tiles.size();
|
||||
m_stats.gpubytes = m_arena.bytes();
|
||||
m_stats.gpubytes = m_arena.usedbytes() + m_widearena.usedbytes();
|
||||
report_residency();
|
||||
}
|
||||
|
||||
@@ -408,7 +478,8 @@ terrain_clipmap::report_residency() {
|
||||
+ 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 "
|
||||
+ std::to_string( m_stats.triangles / 1000 ) + "k triangles out to "
|
||||
+ std::to_string( static_cast<int>( m_stats.furthest ) ) + " m, levels "
|
||||
+ [ this ]() {
|
||||
std::string counts;
|
||||
for( std::uint32_t level = 0; level < m_levels; ++level ) {
|
||||
@@ -418,32 +489,47 @@ terrain_clipmap::report_residency() {
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::render( glm::dvec3 const &Viewpoint, visibility const &Visible ) {
|
||||
terrain_clipmap::render( glm::dvec3 const &Viewpoint, visibility const &Visible, bool const Mainview ) {
|
||||
|
||||
if( ( false == m_ready ) || ( true == m_chosen.empty() ) ) { return; }
|
||||
|
||||
// the ground textures the engine has finished loading since the last frame
|
||||
m_ground.update();
|
||||
|
||||
m_counts.clear();
|
||||
m_offsets.clear();
|
||||
m_bases.clear();
|
||||
for( auto &list : m_counts ) { list.clear(); }
|
||||
for( auto &list : m_offsets ) { list.clear(); }
|
||||
for( auto &list : m_bases ) { list.clear(); }
|
||||
std::size_t triangles { 0 };
|
||||
std::array<std::size_t, terrain_maxlevels> pertile {};
|
||||
std::array<std::size_t, terrain_maxlevels> pertriangle {};
|
||||
auto furthest { 0.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 ) );
|
||||
auto const level { std::min<std::size_t>( tile.level, terrain_maxlevels - 1 ) };
|
||||
++pertile[ level ];
|
||||
pertriangle[ level ] += tile.piece.indices / 3;
|
||||
furthest = std::max(
|
||||
furthest,
|
||||
glm::length( glm::dvec2 { tile.centre.x - Viewpoint.x, tile.centre.z - Viewpoint.z } ) );
|
||||
auto const which { tile.wide ? 1 : 0 };
|
||||
auto const width { tile.wide ? sizeof( std::uint32_t ) : sizeof( std::uint16_t ) };
|
||||
m_counts[ which ].push_back( static_cast<std::int32_t>( tile.piece.indices ) );
|
||||
m_offsets[ which ].push_back(
|
||||
reinterpret_cast<void const *>( static_cast<std::uintptr_t>( tile.piece.firstindex ) * width ) );
|
||||
m_bases[ which ].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; }
|
||||
if( true == Mainview ) {
|
||||
m_stats.drawn = m_counts[ 0 ].size() + m_counts[ 1 ].size();
|
||||
m_stats.triangles = triangles;
|
||||
m_stats.perlevel = pertile;
|
||||
m_stats.trianglesperlevel = pertriangle;
|
||||
m_stats.furthest = furthest;
|
||||
}
|
||||
if( ( true == m_counts[ 0 ].empty() ) && ( true == m_counts[ 1 ].empty() ) ) { return; }
|
||||
|
||||
m_shader->bind();
|
||||
glm::vec3 const origin {
|
||||
@@ -468,12 +554,22 @@ terrain_clipmap::render( glm::dvec3 const &Viewpoint, visibility const &Visible
|
||||
-1.f - 2.f * static_cast<float>( m_depthrank ),
|
||||
-2.f - 4.f * static_cast<float>( m_depthrank ) );
|
||||
|
||||
::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 );
|
||||
if( false == m_counts[ 0 ].empty() ) {
|
||||
m_arena.bind();
|
||||
::glMultiDrawElementsBaseVertex(
|
||||
GL_TRIANGLES, m_counts[ 0 ].data(), GL_UNSIGNED_SHORT,
|
||||
const_cast<void const **>( m_offsets[ 0 ].data() ),
|
||||
static_cast<GLsizei>( m_counts[ 0 ].size() ), m_bases[ 0 ].data() );
|
||||
m_arena.unbind();
|
||||
}
|
||||
if( false == m_counts[ 1 ].empty() ) {
|
||||
m_widearena.bind();
|
||||
::glMultiDrawElementsBaseVertex(
|
||||
GL_TRIANGLES, m_counts[ 1 ].data(), GL_UNSIGNED_INT,
|
||||
const_cast<void const **>( m_offsets[ 1 ].data() ),
|
||||
static_cast<GLsizei>( m_counts[ 1 ].size() ), m_bases[ 1 ].data() );
|
||||
m_widearena.unbind();
|
||||
}
|
||||
|
||||
::glDisable( GL_POLYGON_OFFSET_FILL );
|
||||
::glActiveTexture( GL_TEXTURE1 );
|
||||
|
||||
@@ -61,8 +61,10 @@ public:
|
||||
// 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 tiles the walk settled on that the test says can be seen
|
||||
void render( glm::dvec3 const &Viewpoint, visibility const &Visible );
|
||||
// draws the tiles the walk settled on that the test says can be seen. Mainview says whether this
|
||||
// is the view residency is decided by, and so whether what it drew is worth counting: a shadow
|
||||
// pass sees the same tiles through a frustum of its own
|
||||
void render( glm::dvec3 const &Viewpoint, visibility const &Visible, bool Mainview );
|
||||
|
||||
// how far from the viewpoint terrain is drawn, in metres
|
||||
void range( float const Range ) { m_range = Range; }
|
||||
@@ -86,11 +88,13 @@ public:
|
||||
// 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;
|
||||
float leveledge( std::uint32_t const Level ) const;
|
||||
|
||||
private:
|
||||
// one tile held in the buffers
|
||||
struct resident_tile {
|
||||
terrain_mesh_arena::piece piece;
|
||||
bool wide { false }; // which of the two arenas it is in
|
||||
// for culling, in world space
|
||||
glm::dvec3 centre { 0.0 };
|
||||
float radius { 0.f };
|
||||
@@ -101,7 +105,7 @@ private:
|
||||
// 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
|
||||
// tile's ground can be drawn at all - at the level wanted, or by the tile itself standing in
|
||||
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;
|
||||
@@ -118,7 +122,11 @@ private:
|
||||
|
||||
quantizedmesh::reader m_reader; // the table and which tiles there are; reading is the loader's
|
||||
terrain_tile_loader m_loader;
|
||||
// Nearly every tile fits two-byte indices and lives in the first; a tile of more than sixty-five
|
||||
// thousand vertices - a patch of survey data, usually - goes in the second, which pays four bytes
|
||||
// for them. One draw call each, and the second is empty on most sceneries.
|
||||
terrain_mesh_arena m_arena;
|
||||
terrain_mesh_arena m_widearena;
|
||||
terrain_ground m_ground;
|
||||
|
||||
quantizedmesh::terrain_table m_table;
|
||||
@@ -128,17 +136,24 @@ private:
|
||||
|
||||
std::unordered_map<std::int64_t, resident_tile> m_tiles;
|
||||
std::vector<std::int64_t> m_chosen;
|
||||
// the tiles the walk passed through on its way down. They are not drawn - what is below them is -
|
||||
// but they are held all the same, because the moment the camera turns towards ground whose fine
|
||||
// tiles have not arrived, one of them is what covers it. Without them that ground is a hole until
|
||||
// the fine tiles come, which is what flickering while the camera moves was.
|
||||
std::vector<std::int64_t> m_kept;
|
||||
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;
|
||||
// per tile of the last walk, whether all four of its children can be drawn, and so whether the
|
||||
// walk may go past it. Worked out once and read again when the tiles to draw are picked
|
||||
std::unordered_map<std::int64_t, bool> m_maydescend;
|
||||
|
||||
// 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;
|
||||
// what the draw hands to gl, kept between frames so that a frame allocates nothing. one set for
|
||||
// each arena
|
||||
std::vector<std::int32_t> m_counts[ 2 ];
|
||||
std::vector<void const *> m_offsets[ 2 ];
|
||||
std::vector<std::int32_t> m_bases[ 2 ];
|
||||
|
||||
glm::dvec3 m_walkpoint { 0.0 };
|
||||
bool m_walked { false };
|
||||
@@ -165,7 +180,10 @@ private:
|
||||
unsigned m_depthrank { 0 };
|
||||
// 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 };
|
||||
// Tiles put on the card in one frame. Generous on purpose: what is being absorbed is a read that
|
||||
// has already happened, and the sooner the levels between a new tile and the root are all there,
|
||||
// the shorter the window in which the ground has to be drawn coarse
|
||||
std::size_t m_putsperframe { 64 };
|
||||
bool m_ready { false };
|
||||
std::string m_path;
|
||||
};
|
||||
|
||||
@@ -225,16 +225,24 @@ terrain_ground::update() {
|
||||
entry.copied = true;
|
||||
--m_pending;
|
||||
++copied;
|
||||
// rare, and it changes gl state for a moment: worth a line, so that anything odd seen on
|
||||
// screen can be told apart from what the streamer is doing
|
||||
WriteLog( "Terrain: ground texture " + std::to_string( index ) + " copied into the array, "
|
||||
+ std::to_string( m_pending ) + " still waiting" );
|
||||
}
|
||||
|
||||
gl::vao::unbind();
|
||||
gl::program::bind( 0 );
|
||||
}
|
||||
opengl_texture::reset_unit_cache();
|
||||
|
||||
if( copied > 0 ) {
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, m_array );
|
||||
::glGenerateMipmap( GL_TEXTURE_2D_ARRAY );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, 0 );
|
||||
upload_table();
|
||||
}
|
||||
// last of all, and after the mipmaps rather than before them: the engine remembers what it last
|
||||
// put on each texture unit and skips a bind it believes is already in place, so leaving a bind of
|
||||
// ours behind its back costs it a frame drawn with the wrong texture
|
||||
opengl_texture::reset_unit_cache();
|
||||
}
|
||||
|
||||
@@ -12,22 +12,30 @@ module;
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <glad/glad.h>
|
||||
#include "scene/quantizedmeshreader.h"
|
||||
|
||||
module eu07.rendering.terrainmesharena;
|
||||
import eu07.gl.buffer;
|
||||
import eu07.utilities.logs;
|
||||
import eu07.gl.vao;
|
||||
|
||||
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
|
||||
// what the buffers start at, in vertices and indices, and the most they may reach. a vertex is 16
|
||||
// bytes and an index 4, so the ceiling is about 500 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 };
|
||||
|
||||
constexpr int attribute_position { 0 };
|
||||
constexpr int attribute_material { 1 };
|
||||
constexpr int attribute_normal { 2 };
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void
|
||||
@@ -72,33 +80,44 @@ terrain_mesh_arena::~terrain_mesh_arena() {
|
||||
destroy();
|
||||
}
|
||||
|
||||
// The material is handed over as an integer that the vertex stage takes as a float: gl::vao sets
|
||||
// attributes up one way, and going around it to ask for an integer attribute would put the engine's
|
||||
// idea of what is bound out of step with what is.
|
||||
void
|
||||
terrain_mesh_arena::describe_vertices() {
|
||||
|
||||
auto const stride { static_cast<int>( sizeof( quantizedmesh::render_vertex ) ) };
|
||||
m_vao->setup_attrib(
|
||||
*m_vertexbuffer, attribute_position, 3, GL_FLOAT, stride,
|
||||
static_cast<int>( offsetof( quantizedmesh::render_vertex, x ) ) );
|
||||
m_vao->setup_attrib(
|
||||
*m_vertexbuffer, attribute_material, 1, GL_UNSIGNED_SHORT, stride,
|
||||
static_cast<int>( offsetof( quantizedmesh::render_vertex, material ) ) );
|
||||
// the normal, still folded onto its octahedron; the vertex stage unfolds it
|
||||
m_vao->setup_attrib(
|
||||
*m_vertexbuffer, attribute_normal, 2, GL_UNSIGNED_BYTE, stride,
|
||||
static_cast<int>( offsetof( quantizedmesh::render_vertex, normal0 ) ) );
|
||||
}
|
||||
|
||||
bool
|
||||
terrain_mesh_arena::create() {
|
||||
terrain_mesh_arena::create( bool const Wideindices ) {
|
||||
|
||||
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; }
|
||||
m_wideindices = Wideindices;
|
||||
m_vao.emplace();
|
||||
m_vertexbuffer = std::make_unique<gl::buffer>();
|
||||
m_indexbuffer = std::make_unique<gl::buffer>();
|
||||
|
||||
::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_vao->bind();
|
||||
m_vertexbuffer->allocate(
|
||||
gl::buffer::ARRAY_BUFFER,
|
||||
static_cast<GLsizeiptr>( firstvertices ) * sizeof( quantizedmesh::render_vertex ), GL_DYNAMIC_DRAW );
|
||||
m_indexbuffer->allocate(
|
||||
gl::buffer::ELEMENT_ARRAY_BUFFER,
|
||||
static_cast<GLsizeiptr>( firstindices ) * indexwidth(), GL_DYNAMIC_DRAW );
|
||||
describe_vertices();
|
||||
m_vao->setup_ebo( *m_indexbuffer );
|
||||
unbind();
|
||||
|
||||
m_vertexroom = firstvertices;
|
||||
m_indexroom = firstindices;
|
||||
@@ -113,17 +132,37 @@ terrain_mesh_arena::create() {
|
||||
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; }
|
||||
if( true == m_vao.has_value() ) { m_vao->unbind(); }
|
||||
gl::buffer::unbind( gl::buffer::ARRAY_BUFFER );
|
||||
gl::buffer::unbind( gl::buffer::ELEMENT_ARRAY_BUFFER );
|
||||
m_vao.reset();
|
||||
m_vertexbuffer.reset();
|
||||
m_indexbuffer.reset();
|
||||
m_vertexroom = m_indexroom = 0;
|
||||
m_freevertices.clear();
|
||||
m_freeindices.clear();
|
||||
m_verticesheld = 0;
|
||||
m_indicesheld = 0;
|
||||
}
|
||||
|
||||
void
|
||||
terrain_mesh_arena::bind() {
|
||||
|
||||
if( false == m_vao.has_value() ) { return; }
|
||||
m_vao->bind();
|
||||
}
|
||||
|
||||
void
|
||||
terrain_mesh_arena::unbind() {
|
||||
|
||||
if( true == m_vao.has_value() ) { m_vao->unbind(); }
|
||||
gl::buffer::unbind( gl::buffer::ARRAY_BUFFER );
|
||||
gl::buffer::unbind( gl::buffer::COPY_READ_BUFFER );
|
||||
gl::buffer::unbind( gl::buffer::COPY_WRITE_BUFFER );
|
||||
}
|
||||
|
||||
// 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.
|
||||
// nothing has to be read back or sent again.
|
||||
bool
|
||||
terrain_mesh_arena::grow_vertices( std::uint32_t const Wanted ) {
|
||||
|
||||
@@ -131,32 +170,27 @@ terrain_mesh_arena::grow_vertices( std::uint32_t const Wanted ) {
|
||||
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 );
|
||||
auto grown { std::make_unique<gl::buffer>() };
|
||||
grown->allocate(
|
||||
gl::buffer::COPY_WRITE_BUFFER,
|
||||
static_cast<GLsizeiptr>( room ) * sizeof( quantizedmesh::render_vertex ), GL_DYNAMIC_DRAW );
|
||||
m_vertexbuffer->bind( gl::buffer::COPY_READ_BUFFER );
|
||||
::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;
|
||||
gl::buffer::unbind( gl::buffer::COPY_READ_BUFFER );
|
||||
gl::buffer::unbind( gl::buffer::COPY_WRITE_BUFFER );
|
||||
|
||||
::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 );
|
||||
// the tracker has to forget the buffer before it is destroyed: gl reuses the name of a deleted
|
||||
// buffer, so the next one can come back with the same number, and a bind of it would be skipped as
|
||||
// already in place while nothing is actually bound
|
||||
gl::buffer::unbind( gl::buffer::ARRAY_BUFFER );
|
||||
m_vertexbuffer = std::move( grown );
|
||||
m_vao->bind();
|
||||
describe_vertices();
|
||||
|
||||
m_freevertices.give( m_vertexroom, room - m_vertexroom );
|
||||
WriteLog( "Terrain: vertex buffer grown from " + std::to_string( m_vertexroom ) + " to "
|
||||
+ std::to_string( room ) + " vertices" );
|
||||
m_vertexroom = room;
|
||||
return true;
|
||||
}
|
||||
@@ -168,27 +202,24 @@ terrain_mesh_arena::grow_indices( std::uint32_t const Wanted ) {
|
||||
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 );
|
||||
auto grown { std::make_unique<gl::buffer>() };
|
||||
grown->allocate(
|
||||
gl::buffer::COPY_WRITE_BUFFER,
|
||||
static_cast<GLsizeiptr>( room ) * indexwidth(), GL_DYNAMIC_DRAW );
|
||||
m_indexbuffer->bind( gl::buffer::COPY_READ_BUFFER );
|
||||
::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;
|
||||
static_cast<GLsizeiptr>( m_indexroom ) * indexwidth() );
|
||||
gl::buffer::unbind( gl::buffer::COPY_READ_BUFFER );
|
||||
gl::buffer::unbind( gl::buffer::COPY_WRITE_BUFFER );
|
||||
|
||||
::glBindVertexArray( m_vao );
|
||||
::glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, m_indexbuffer );
|
||||
::glBindVertexArray( 0 );
|
||||
::glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
|
||||
gl::buffer::unbind( gl::buffer::ELEMENT_ARRAY_BUFFER );
|
||||
m_indexbuffer = std::move( grown );
|
||||
m_vao->bind();
|
||||
m_vao->setup_ebo( *m_indexbuffer );
|
||||
|
||||
m_freeindices.give( m_indexroom, room - m_indexroom );
|
||||
WriteLog( "Terrain: index buffer grown from " + std::to_string( m_indexroom ) + " to "
|
||||
+ std::to_string( room ) + " indices" );
|
||||
m_indexroom = room;
|
||||
return true;
|
||||
}
|
||||
@@ -197,9 +228,10 @@ 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() ) ) {
|
||||
if( ( false == m_vao.has_value() ) || ( nullptr == m_vertexbuffer ) || ( true == Tile.vertices.empty() ) || ( true == Tile.indices.empty() ) ) {
|
||||
return where;
|
||||
}
|
||||
if( Tile.vertices.size() > vertexlimit() ) { return where; }
|
||||
auto const vertices { static_cast<std::uint32_t>( Tile.vertices.size() ) };
|
||||
auto const indices { static_cast<std::uint32_t>( Tile.indices.size() ) };
|
||||
|
||||
@@ -220,27 +252,36 @@ terrain_mesh_arena::put( quantizedmesh::reader::tile_data const &Tile ) {
|
||||
}
|
||||
}
|
||||
|
||||
::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 );
|
||||
// this vertex array first: which element buffer is bound belongs to it, and writing indices
|
||||
// with the scenery's array bound would take its own indices away
|
||||
m_vao->bind();
|
||||
m_vertexbuffer->upload(
|
||||
gl::buffer::ARRAY_BUFFER, Tile.vertices.data(),
|
||||
static_cast<int>( atvertex * sizeof( quantizedmesh::render_vertex ) ),
|
||||
static_cast<GLsizeiptr>( vertices ) * sizeof( quantizedmesh::render_vertex ) );
|
||||
// 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
|
||||
if( true == m_wideindices ) {
|
||||
m_indexbuffer->upload(
|
||||
gl::buffer::ELEMENT_ARRAY_BUFFER, Tile.indices.data(),
|
||||
static_cast<int>( atindex * sizeof( std::uint32_t ) ),
|
||||
static_cast<GLsizeiptr>( indices ) * sizeof( std::uint32_t ) );
|
||||
}
|
||||
else {
|
||||
m_narrow.assign( Tile.indices.begin(), Tile.indices.end() );
|
||||
m_indexbuffer->upload(
|
||||
gl::buffer::ELEMENT_ARRAY_BUFFER, m_narrow.data(),
|
||||
static_cast<int>( atindex * sizeof( std::uint16_t ) ),
|
||||
static_cast<GLsizeiptr>( indices ) * sizeof( std::uint16_t ) );
|
||||
}
|
||||
unbind();
|
||||
|
||||
where.firstvertex = atvertex;
|
||||
where.vertices = vertices;
|
||||
where.firstindex = atindex;
|
||||
where.indices = indices;
|
||||
m_verticesheld += vertices;
|
||||
m_indicesheld += indices;
|
||||
return where;
|
||||
}
|
||||
|
||||
@@ -251,5 +292,6 @@ terrain_mesh_arena::take( piece &Piece ) {
|
||||
m_freevertices.give( Piece.firstvertex, Piece.vertices );
|
||||
m_freeindices.give( Piece.firstindex, Piece.indices );
|
||||
m_verticesheld -= Piece.vertices;
|
||||
m_indicesheld -= Piece.indices;
|
||||
Piece = piece {};
|
||||
}
|
||||
|
||||
@@ -11,10 +11,14 @@ module;
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "scene/quantizedmeshreader.h"
|
||||
|
||||
export module eu07.rendering.terrainmesharena;
|
||||
import eu07.gl.buffer;
|
||||
import eu07.gl.vao;
|
||||
|
||||
export {
|
||||
|
||||
@@ -26,7 +30,18 @@ export {
|
||||
// bind and a draw per tile. Vertices are measured from one point for the whole terrain, which is
|
||||
// what makes that possible.
|
||||
//
|
||||
// Indices are two bytes wide unless the arena is asked for four. An index is a quarter of a vertex but
|
||||
// there are some six of them per vertex, so they are the larger half of what the terrain costs on the
|
||||
// card - and a tile of fewer than sixty-five thousand vertices, which is nearly every tile, has no use
|
||||
// for four bytes. The few that need more get an arena of their own and a draw call of their own.
|
||||
//
|
||||
// The buffers grow when a tile will not fit and nothing can be freed; they never shrink.
|
||||
//
|
||||
// Everything here goes through gl::vao and gl::buffer rather than through glBindVertexArray and
|
||||
// glBindBuffer. The engine remembers what it last bound and skips a bind it believes is already in
|
||||
// place, so a raw bind here leaves it drawing the scenery's geometry through this vertex array. And
|
||||
// the element buffer binding belongs to whichever vertex array is bound, so writing indices without
|
||||
// binding this one first overwrites the scenery's.
|
||||
class terrain_mesh_arena {
|
||||
|
||||
public:
|
||||
@@ -44,19 +59,29 @@ public:
|
||||
terrain_mesh_arena( terrain_mesh_arena const & ) = delete;
|
||||
terrain_mesh_arena &operator=( terrain_mesh_arena const & ) = delete;
|
||||
|
||||
bool create();
|
||||
// Wideindices asks for four-byte indices, for tiles of more than 65536 vertices
|
||||
bool create( bool Wideindices );
|
||||
void destroy();
|
||||
bool wideindices() const { return m_wideindices; }
|
||||
// the most vertices a tile may have to fit this arena
|
||||
std::uint32_t vertexlimit() const { return m_wideindices ? ( 1u << 24 ) : 65536u; }
|
||||
|
||||
// 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; }
|
||||
void bind();
|
||||
void unbind();
|
||||
|
||||
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(); }
|
||||
// what the two buffers were allocated at. it only grows: gl has no way of giving a buffer back
|
||||
// except by making a new one, and the pieces already in it would have to move
|
||||
std::size_t indexwidth() const { return m_wideindices ? sizeof( std::uint32_t ) : sizeof( std::uint16_t ); }
|
||||
std::size_t roombytes() const {
|
||||
return m_vertexroom * sizeof( quantizedmesh::render_vertex ) + m_indexroom * indexwidth(); }
|
||||
// and what the tiles in them actually take up, which is what a budget has to be measured against
|
||||
std::size_t usedbytes() const {
|
||||
return m_verticesheld * sizeof( quantizedmesh::render_vertex ) + m_indicesheld * indexwidth(); }
|
||||
std::size_t verticesheld() const { return m_verticesheld; }
|
||||
|
||||
private:
|
||||
@@ -74,14 +99,22 @@ private:
|
||||
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 };
|
||||
// attribute places the shader reads, and what one vertex is made of
|
||||
void describe_vertices();
|
||||
|
||||
// gl::buffer cannot be moved - it deletes what it holds - so growing one means putting a new one
|
||||
// in its place rather than handing it over
|
||||
std::optional<gl::vao> m_vao;
|
||||
std::unique_ptr<gl::buffer> m_vertexbuffer;
|
||||
std::unique_ptr<gl::buffer> m_indexbuffer;
|
||||
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 };
|
||||
std::size_t m_indicesheld { 0 };
|
||||
bool m_wideindices { false };
|
||||
std::vector<std::uint16_t> m_narrow; // scratch for the common case, so a put allocates nothing
|
||||
};
|
||||
|
||||
} // export
|
||||
|
||||
@@ -30,10 +30,33 @@ export {
|
||||
// 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 };
|
||||
|
||||
// A triangle may be about this much of the error budget across before it is worth splitting the tile. The error alone will not do the choosing: it is the worst departure anywhere in the tile and
|
||||
// every level above inherits it, so one steep corner sets the same figure for the whole pyramid and
|
||||
// the levels become indistinguishable - a terrain of sixty triangles and one of fifteen thousand ask
|
||||
// to be drawn from the same distance. What tells them apart is how large the triangles are.
|
||||
inline constexpr double terrain_edge_share { 0.1 };
|
||||
|
||||
// what a level asks for, in metres: the worse of how far its ground is off and how large its
|
||||
// triangles are
|
||||
inline double
|
||||
terrain_level_demand( double const Error, double const Edge ) {
|
||||
return std::max( Error, Edge * terrain_edge_share );
|
||||
}
|
||||
|
||||
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 );
|
||||
double const Error, double const Edge, double const Distance,
|
||||
double const Pixelsperunit, double const Detail ) {
|
||||
return terrain_level_demand( Error, Edge ) * Pixelsperunit
|
||||
<= std::max( 0.1, Detail ) * std::max( 1.0, Distance );
|
||||
}
|
||||
|
||||
// the other way round: how far away a level has to be before it is good enough. what the detail
|
||||
// setting means in metres, which is the only form it can be judged in
|
||||
inline double
|
||||
terrain_level_from(
|
||||
double const Error, double const Edge, double const Pixelsperunit, double const Detail ) {
|
||||
return terrain_level_demand( Error, Edge ) * Pixelsperunit / std::max( 0.1, Detail );
|
||||
}
|
||||
|
||||
// how far the camera may move before which tiles are wanted is worked out again, as a share of the
|
||||
@@ -50,7 +73,9 @@ struct terrain_statistics {
|
||||
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
|
||||
std::array<std::size_t, terrain_maxlevels> perlevel {}; // tiles drawn at each level
|
||||
std::array<std::size_t, terrain_maxlevels> trianglesperlevel {}; // and the triangles in them
|
||||
double furthest { 0.0 }; // how far the furthest tile drawn reaches, in metres
|
||||
};
|
||||
|
||||
// one terrain as the debug panel shows it. the renderer writes it once a frame from the main view,
|
||||
@@ -60,10 +85,12 @@ struct terrain_field_status {
|
||||
float tilesize { 0.f }; // side of a tile at the finest level, in metres
|
||||
float range { 0.f };
|
||||
float detail { 0.f }; // pixels an error may cover before a finer level is used
|
||||
float pixelsperunit { 0.f }; // what a metre at a metre's distance spans on this screen
|
||||
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
|
||||
std::array<float, terrain_maxlevels> leveledge {}; // and how long its triangles typically are
|
||||
terrain_statistics stats;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user