mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-28 21:37:31 +01:00
Draw terrain one call per level, from texture arrays, culled to the view
Each resident tile had a height texture and a material texture of its own, uploaded with glTexImage2D, and was drawn once per material it held with every other material's fragments discarded, all of them whether in view or not. Thousands of draws a frame. Now the tiles of a level live in one pair of texture arrays (terrain_tile_pool), a layer per tile. A pool starts at sixteen layers and doubles when full, copying what it holds on the gpu, so a level takes only the memory its tiles need; uploads go through a pixel buffer orphaned for each tile. The ground textures of every material are gathered into one sRGB array with mipmaps (terrain_ground) - drawn into it layer by layer by a small shader once the engine has each texture ready, since the scenery's textures are compressed and cannot be blitted - with a table of placeholder colours and measured tiling beside it. Each frame the tiles in range are tested against the frustum by bounding sphere and gathered per level into a buffer texture of instance data, and each level is drawn in one instanced call. The fragment shader reads the four samples around it, drops the cell when any of them carries no ground - exactly the rule the bake already assumed - and blends the ground textures of their materials by distance, so material boundaries follow the samples smoothly instead of stepping along triangle edges. State the copy pass changes is captured and put back, and the texture unit cache is reset after raw binds, so the rest of the renderer finds things as it left them. On baltyk: all 30 materials land in the array, 663 of the 1120 tiles in range are drawn after culling, and a frame is at most six terrain draws. The residency line in the log now says how many tiles in range were drawn.
This commit is contained in:
@@ -2919,7 +2919,9 @@ void opengl33_renderer::Render_terrain()
|
||||
Global.TerrainDetail);
|
||||
field->update(viewpoint);
|
||||
}
|
||||
field->render(viewpoint);
|
||||
field->render(viewpoint, [this](glm::dvec3 const &Centre, float const Radius) {
|
||||
return m_renderpass.pass_camera.visible(scene::bounding_area(Centre, Radius));
|
||||
});
|
||||
}
|
||||
|
||||
if (m_current_viewport->main)
|
||||
|
||||
@@ -22,37 +22,13 @@ module;
|
||||
#include "glad/glad.h"
|
||||
|
||||
#include "scene/heightfieldformat.h"
|
||||
#include "global_include/interfaces/ITexture_macros.h"
|
||||
#include "scene/heightfieldreader.h"
|
||||
|
||||
module eu07.rendering.terrainclipmap;
|
||||
import eu07.glm;
|
||||
import eu07.utilities.logs;
|
||||
import eu07.rendering.renderer;
|
||||
import eu07.model.material;
|
||||
import eu07.model.texture;
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
// NOTE: glTexImage2D rather than glTexStorage2D. Immutable storage arrives with OpenGL
|
||||
// 4.2 and this renderer holds a 3.3 context, where the entry point is simply not there -
|
||||
// calling it jumps through a null pointer instead of failing in any visible way
|
||||
GLuint
|
||||
create_texture( GLenum const Internalformat, GLenum const Type, GLsizei const Side, void const *Data ) {
|
||||
|
||||
GLuint texture { 0 };
|
||||
::glGenTextures( 1, &texture );
|
||||
::glBindTexture( GL_TEXTURE_2D, texture );
|
||||
// integer samples carry the cooker's values, no-data included, so nothing may filter them
|
||||
::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
|
||||
::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
|
||||
::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
|
||||
::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
|
||||
::glTexImage2D( GL_TEXTURE_2D, 0, Internalformat, Side, Side, 0, GL_RED_INTEGER, Type, Data );
|
||||
return texture;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
terrain_clipmap::~terrain_clipmap() {
|
||||
close();
|
||||
@@ -72,8 +48,6 @@ terrain_clipmap::open( std::string const &Path ) {
|
||||
|
||||
m_path = Path;
|
||||
build_indices();
|
||||
build_palette();
|
||||
resolve_materials();
|
||||
|
||||
try {
|
||||
gl::shader vertex( "terrainclipmap.vert" );
|
||||
@@ -85,6 +59,18 @@ terrain_clipmap::open( std::string const &Path ) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if( false == m_ground.create( m_reader.materials(), m_reader.material_scales() ) ) {
|
||||
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();
|
||||
@@ -98,28 +84,23 @@ terrain_clipmap::open( std::string const &Path ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// uniform locations do not change once the program is linked, so they are looked up
|
||||
// here rather than per tile per frame
|
||||
// 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.tileorigin = ::glGetUniformLocation( program, "tileorigin" );
|
||||
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.side = ::glGetUniformLocation( program, "side" );
|
||||
m_uniforms.morph = ::glGetUniformLocation( program, "morph" );
|
||||
m_uniforms.heights = ::glGetUniformLocation( program, "heights" );
|
||||
m_uniforms.materials = ::glGetUniformLocation( program, "materials" );
|
||||
m_uniforms.palette = ::glGetUniformLocation( program, "palette" );
|
||||
m_uniforms.groundtexture = ::glGetUniformLocation( program, "ground" );
|
||||
m_uniforms.groundscale = ::glGetUniformLocation( program, "groundscale" );
|
||||
m_uniforms.drawnmaterial = ::glGetUniformLocation( program, "drawnmaterial" );
|
||||
m_uniforms.tileworld = ::glGetUniformLocation( program, "tileworld" );
|
||||
m_uniforms.hastexture = ::glGetUniformLocation( program, "hastexture" );
|
||||
m_uniforms.fallback = ::glGetUniformLocation( program, "fallback" );
|
||||
m_uniforms.instances = ::glGetUniformLocation( program, "instances" );
|
||||
m_uniforms.ground = ::glGetUniformLocation( program, "ground" );
|
||||
m_uniforms.materialtable = ::glGetUniformLocation( program, "materialtable" );
|
||||
}
|
||||
|
||||
void
|
||||
@@ -132,13 +113,16 @@ terrain_clipmap::close() {
|
||||
m_inrange.clear();
|
||||
m_wantedlevel.clear();
|
||||
m_scanrange = -1.f;
|
||||
for( auto & [ tilekey, tile ] : m_tiles ) { retire( tile ); }
|
||||
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_ground.destroy();
|
||||
m_vao.reset();
|
||||
m_shader.reset();
|
||||
m_palette.clear();
|
||||
m_reader.close();
|
||||
m_stats = {};
|
||||
m_ready = false;
|
||||
@@ -180,30 +164,6 @@ terrain_clipmap::build_indices() {
|
||||
}
|
||||
}
|
||||
|
||||
// placeholder colours, one per cooked material, so that the shape of the terrain and the
|
||||
// division into surfaces can be judged before the real materials are wired in
|
||||
void
|
||||
terrain_clipmap::build_palette() {
|
||||
|
||||
m_palette.clear();
|
||||
m_palette.reserve( std::max<std::size_t>( 1, m_reader.materials().size() ) );
|
||||
for( std::size_t index = 0; index < m_reader.materials().size(); ++index ) {
|
||||
auto const hue { static_cast<float>( index ) * 0.61803399f };
|
||||
auto const phase { ( hue - std::floor( hue ) ) * 6.f };
|
||||
auto const rising { phase - std::floor( phase ) };
|
||||
glm::vec3 colour { 0.45f, 0.5f, 0.4f };
|
||||
switch( static_cast<int>( phase ) ) {
|
||||
case 0: colour = { 0.55f, 0.45f + 0.2f * rising, 0.3f }; break;
|
||||
case 1: colour = { 0.55f - 0.2f * rising, 0.6f, 0.3f }; break;
|
||||
case 2: colour = { 0.35f, 0.6f, 0.3f + 0.2f * rising }; break;
|
||||
case 3: colour = { 0.35f, 0.6f - 0.15f * rising, 0.5f }; break;
|
||||
case 4: colour = { 0.35f + 0.2f * rising, 0.45f, 0.5f }; break;
|
||||
default: colour = { 0.55f, 0.45f, 0.5f - 0.15f * rising }; break;
|
||||
}
|
||||
m_palette.push_back( colour );
|
||||
}
|
||||
if( true == m_palette.empty() ) { m_palette.push_back( { 0.45f, 0.5f, 0.4f } ); }
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
terrain_clipmap::level_for( double const Distance ) const {
|
||||
@@ -217,51 +177,35 @@ void
|
||||
terrain_clipmap::upload( terrain_tile_loader::payload const &Tile ) {
|
||||
|
||||
auto const tilekey { key( Tile.tile.x, Tile.tile.z ) };
|
||||
auto const side { m_reader.samples_per_side( Tile.tile.level ) };
|
||||
|
||||
auto const existing { m_tiles.find( tilekey ) };
|
||||
if( existing != m_tiles.end() ) {
|
||||
// a level change means new textures: the sample count differs
|
||||
// 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.side = side;
|
||||
tile.slot = pool.acquire();
|
||||
tile.lastused = m_frame;
|
||||
tile.heighttexture = create_texture(
|
||||
GL_R16UI, GL_UNSIGNED_SHORT, static_cast<GLsizei>( side ), Tile.heights.data() );
|
||||
tile.materialtexture = create_texture(
|
||||
GL_R8UI, GL_UNSIGNED_BYTE, static_cast<GLsizei>( side ), Tile.materials.data() );
|
||||
|
||||
// the tile is drawn once per material it contains, so the set is worked out here
|
||||
// rather than rescanned every frame
|
||||
std::array<bool, 256> present {};
|
||||
for( auto const material : Tile.materials ) { present[ material ] = true; }
|
||||
for( std::size_t material = 0; material < present.size(); ++material ) {
|
||||
if( true == present[ material ] ) { tile.materials.push_back( static_cast<std::uint8_t>( material ) ); }
|
||||
}
|
||||
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.texturebytes += static_cast<std::size_t>( side ) * side * 3u;
|
||||
++m_stats.uploaded;
|
||||
m_tiles.emplace( tilekey, std::move( tile ) );
|
||||
m_tiles.emplace( tilekey, tile );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::retire( resident_tile &Tile ) {
|
||||
terrain_clipmap::retire( resident_tile const &Tile ) {
|
||||
|
||||
if( Tile.heighttexture != 0 ) { ::glDeleteTextures( 1, &Tile.heighttexture ); Tile.heighttexture = 0; }
|
||||
if( Tile.materialtexture != 0 ) { ::glDeleteTextures( 1, &Tile.materialtexture ); Tile.materialtexture = 0; }
|
||||
auto const bytes { static_cast<std::size_t>( Tile.side ) * Tile.side * 3u };
|
||||
m_stats.texturebytes -= std::min( m_stats.texturebytes, bytes );
|
||||
m_pools[ Tile.level ]->release( Tile.slot );
|
||||
}
|
||||
|
||||
void
|
||||
@@ -395,50 +339,11 @@ terrain_clipmap::update( glm::dvec3 const &Viewpoint ) {
|
||||
}
|
||||
|
||||
m_stats.resident = m_tiles.size();
|
||||
m_stats.texturebytes = 0;
|
||||
for( auto const &pool : m_pools ) { m_stats.texturebytes += pool->bytes(); }
|
||||
report_residency();
|
||||
}
|
||||
|
||||
// the cooked material names are the ones the scenery used, so the engine resolves them
|
||||
// exactly as it does for any other geometry. only the diffuse texture is taken: the
|
||||
// terrain has its own shader and none of the rest of a material applies to it
|
||||
void
|
||||
terrain_clipmap::resolve_materials() {
|
||||
|
||||
m_materials.clear();
|
||||
auto const &names { m_reader.materials() };
|
||||
auto const &scales { m_reader.material_scales() };
|
||||
m_materials.reserve( names.size() );
|
||||
|
||||
std::size_t resolved { 0 };
|
||||
std::string unresolved;
|
||||
for( std::size_t index = 0; index < names.size(); ++index ) {
|
||||
|
||||
material_binding binding;
|
||||
binding.scale = ( index < scales.size() ? scales[ index ] : 4.f );
|
||||
if( binding.scale < 0.01f ) { binding.scale = 4.f; }
|
||||
|
||||
auto const material { GfxRenderer->Fetch_Material( names[ index ] ) };
|
||||
if( material != null_handle ) {
|
||||
binding.texture = GfxRenderer->Material( material )->GetTexture( 0 );
|
||||
}
|
||||
if( binding.texture != null_handle ) {
|
||||
++resolved;
|
||||
}
|
||||
else {
|
||||
// drawn in its placeholder colour instead, so a missing texture shows up as
|
||||
// an obvious flat patch rather than as black ground
|
||||
unresolved += ( unresolved.empty() ? "" : ", " ) + names[ index ];
|
||||
}
|
||||
m_materials.push_back( binding );
|
||||
}
|
||||
|
||||
WriteLog(
|
||||
"Terrain: " + std::to_string( resolved ) + " of " + std::to_string( names.size() )
|
||||
+ " materials resolved to textures" );
|
||||
if( false == unresolved.empty() ) {
|
||||
WriteLog( "Terrain: no texture for " + unresolved );
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -457,15 +362,45 @@ terrain_clipmap::report_residency() {
|
||||
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, finest level out to "
|
||||
+ 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" );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_clipmap::render( glm::dvec3 const &Viewpoint ) {
|
||||
terrain_clipmap::render( glm::dvec3 const &Viewpoint, visibility const &Visible ) {
|
||||
|
||||
if( ( false == m_ready ) || ( true == m_tiles.empty() ) ) { return; }
|
||||
|
||||
// textures that finished loading since the last frame go into the ground array first
|
||||
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 ) - m_depthbias,
|
||||
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_shader->bind();
|
||||
m_vao->bind();
|
||||
|
||||
@@ -474,69 +409,62 @@ terrain_clipmap::render( glm::dvec3 const &Viewpoint ) {
|
||||
::glUniform1ui( m_uniforms.nodata, heightfield::nodata );
|
||||
::glUniform1i( m_uniforms.heights, 0 );
|
||||
::glUniform1i( m_uniforms.materials, 1 );
|
||||
::glUniform1i( m_uniforms.groundtexture, 2 );
|
||||
::glUniform3fv(
|
||||
m_uniforms.palette, static_cast<GLsizei>( m_palette.size() ), &m_palette.front().x );
|
||||
::glUniform1i( m_uniforms.ground, 2 );
|
||||
::glUniform1i( m_uniforms.materialtable, 3 );
|
||||
::glUniform1i( m_uniforms.instances, 4 );
|
||||
|
||||
auto const tilesize { static_cast<double>( m_reader.tilesize() ) };
|
||||
::glActiveTexture( GL_TEXTURE2 );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, m_ground.textures() );
|
||||
::glActiveTexture( GL_TEXTURE3 );
|
||||
::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 ) {
|
||||
|
||||
for( auto const & [ tilekey, tile ] : m_tiles ) {
|
||||
auto const &instances { m_instances[ level ] };
|
||||
if( true == instances.empty() ) { continue; }
|
||||
auto const count { instances.size() / 8 };
|
||||
drawn += count;
|
||||
|
||||
if( tile.lastused != m_frame ) { continue; }
|
||||
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 ) );
|
||||
|
||||
// tile position relative to the viewpoint, so that the shader never sees a
|
||||
// coordinate large enough for single precision to matter
|
||||
glm::vec3 const origin {
|
||||
static_cast<float>( tile.x * tilesize - Viewpoint.x ),
|
||||
static_cast<float>( -Viewpoint.y ) - m_depthbias,
|
||||
static_cast<float>( tile.z * tilesize - Viewpoint.z ) };
|
||||
::glUniform3fv( m_uniforms.tileorigin, 1, &origin.x );
|
||||
::glUniform1f( m_uniforms.samplestep, gridstep * static_cast<float>( 1u << tile.level ) );
|
||||
::glUniform1i( m_uniforms.side, static_cast<GLint>( tile.side ) );
|
||||
// the band over which this level turns into the next. the coarsest level has no
|
||||
// next one, so it never morphs
|
||||
if( tile.level + 1 < m_reader.levels() ) {
|
||||
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( tile.level, m_finest, tilesize ) ),
|
||||
static_cast<float>( terrain_morph_end( tile.level, m_finest, tilesize ) ) );
|
||||
static_cast<float>( terrain_morph_begin( level, m_finest, tilesize ) ),
|
||||
static_cast<float>( terrain_morph_end( level, m_finest, tilesize ) ) );
|
||||
}
|
||||
else {
|
||||
// far enough to never be reached, with a band that does not divide by zero
|
||||
::glUniform2f( m_uniforms.morph, 1e30f, 2e30f );
|
||||
}
|
||||
::glUniform2f(
|
||||
m_uniforms.tileworld,
|
||||
static_cast<float>( tile.x * tilesize ), static_cast<float>( tile.z * tilesize ) );
|
||||
|
||||
::glActiveTexture( GL_TEXTURE0 );
|
||||
::glBindTexture( GL_TEXTURE_2D, tile.heighttexture );
|
||||
::glActiveTexture( GL_TEXTURE1 );
|
||||
::glBindTexture( GL_TEXTURE_2D, tile.materialtexture );
|
||||
|
||||
m_indices[ tile.level ]->bind( gl::buffer::ELEMENT_ARRAY_BUFFER );
|
||||
|
||||
// one pass per material in the tile: each wants a different texture bound, and
|
||||
// a fragment belonging to any other material is dropped
|
||||
for( auto const material : tile.materials ) {
|
||||
|
||||
auto const &binding {
|
||||
material < m_materials.size() ? m_materials[ material ] : material_binding {} };
|
||||
GfxRenderer->Bind_Texture( 2, binding.texture );
|
||||
::glUniform1f( m_uniforms.groundscale, binding.scale );
|
||||
::glUniform1ui( m_uniforms.drawnmaterial, material );
|
||||
::glUniform1i( m_uniforms.hastexture, binding.texture != null_handle ? 1 : 0 );
|
||||
auto const &fallback {
|
||||
m_palette[ std::min<std::size_t>( material, m_palette.size() - 1 ) ] };
|
||||
::glUniform3fv( m_uniforms.fallback, 1, &fallback.x );
|
||||
|
||||
::glDrawElements(
|
||||
GL_TRIANGLES, static_cast<GLsizei>( m_indexcounts[ tile.level ] ), GL_UNSIGNED_INT, nullptr );
|
||||
}
|
||||
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;
|
||||
|
||||
gl::buffer::unbind( gl::buffer::TEXTURE_BUFFER );
|
||||
::glActiveTexture( GL_TEXTURE0 );
|
||||
opengl_texture::reset_unit_cache();
|
||||
m_vao->unbind();
|
||||
gl::program::bind( 0 );
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ http://mozilla.org/MPL/2.0/.
|
||||
module;
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "global_include/interfaces/ITexture_macros.h"
|
||||
#include "scene/heightfieldreader.h"
|
||||
|
||||
export module eu07.rendering.terrainclipmap;
|
||||
@@ -26,26 +26,34 @@ import eu07.gl.vao;
|
||||
import eu07.glm;
|
||||
import eu07.rendering.terraintileloader;
|
||||
import eu07.rendering.terrainstatus;
|
||||
import eu07.model.material;
|
||||
import eu07.global_include.interfaces.itexture;
|
||||
import eu07.rendering.terraintilepool;
|
||||
import eu07.rendering.terrainground;
|
||||
|
||||
export {
|
||||
|
||||
// Draws the cooked terrain heightfield.
|
||||
//
|
||||
// Not a toroidal clipmap, despite what the plan called it. The measurements say the
|
||||
// terrain is a ribbon: 390 km2 of surface inside a 51 x 46 km rectangle, twelve percent
|
||||
// of the tile bounding box occupied. Clipmap rings sized for that rectangle would be
|
||||
// mostly empty, so this draws the cooked tiles themselves, each at the mip level its
|
||||
// distance earns. The tiles were laid out for exactly this - sorted by morton code, one
|
||||
// shared sample of overlap so neighbouring levels meet without skirts.
|
||||
// Not a toroidal clipmap, despite what the plan called it. The measurements say the terrain is
|
||||
// a ribbon: 390 km2 of surface inside a 51 x 46 km rectangle, twelve percent of the tile
|
||||
// bounding box occupied. Clipmap rings sized for that rectangle would be mostly empty, so this
|
||||
// draws the cooked tiles themselves, each at the mip level its distance earns (CDLOD), morphing
|
||||
// into the next level as it nears the end of its range.
|
||||
//
|
||||
// The class owns its gl resources and the file handle, and knows nothing about the
|
||||
// scenery, the camera class or the rest of the renderer: it is given a viewpoint and a
|
||||
// pair of matrices, and draws.
|
||||
// Tiles live on the gpu in one texture array per level (terrain_tile_pool), and the ground
|
||||
// textures of every material in another (terrain_ground). Each frame the tiles in view are
|
||||
// culled against the frustum and gathered per level into a buffer of instance data, and each
|
||||
// level is drawn in one instanced call; the fragment shader picks and blends the materials of
|
||||
// the four samples around it.
|
||||
//
|
||||
// The class owns its gl resources and the file handle, and knows nothing about the scenery,
|
||||
// the camera class or the rest of the renderer: it is given a viewpoint and a visibility test,
|
||||
// and draws.
|
||||
class terrain_clipmap {
|
||||
|
||||
public:
|
||||
// whether a sphere, in world space, can be seen: centre and radius
|
||||
using visibility = std::function<bool( glm::dvec3 const &, float )>;
|
||||
|
||||
terrain_clipmap() = default;
|
||||
~terrain_clipmap();
|
||||
terrain_clipmap( terrain_clipmap const & ) = delete;
|
||||
@@ -62,11 +70,11 @@ public:
|
||||
// and decoding happen on the loader's thread, and a tile waiting for a finer level keeps
|
||||
// being drawn at the level it has
|
||||
void update( glm::dvec3 const &Viewpoint );
|
||||
// draws the resident tiles. the caller is expected to have the scene and model
|
||||
// uniform blocks already set for a camera-relative pass, the way the rest of the
|
||||
// renderer does: tile positions reach the shader relative to the viewpoint, so
|
||||
// nothing here needs a matrix of its own
|
||||
void render( 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, the way the rest of the renderer does: tile positions reach the shader relative
|
||||
// to the viewpoint, so nothing here needs a matrix of its own
|
||||
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; }
|
||||
@@ -91,19 +99,19 @@ public:
|
||||
std::size_t backlog() const { return m_loader.backlog(); }
|
||||
|
||||
private:
|
||||
// one tile resident on the gpu. heights are unsigned so that the no-data value stays
|
||||
// exactly what the cooker wrote instead of being normalised into a float
|
||||
// one tile resident on the gpu: a layer in its level's pool
|
||||
struct resident_tile {
|
||||
std::int32_t x { 0 }, z { 0 };
|
||||
std::uint32_t level { 0 }; // mip level currently uploaded
|
||||
std::uint32_t side { 0 }; // samples per side at that level
|
||||
std::uint32_t heighttexture { 0 };
|
||||
std::uint32_t materialtexture { 0 };
|
||||
std::uint32_t level { 0 };
|
||||
std::uint32_t slot { 0 };
|
||||
float minheight { 0.f }, maxheight { 0.f };
|
||||
std::uint64_t lastused { 0 };
|
||||
// materials appearing in this tile. a tile is drawn once per entry, since each
|
||||
// needs its own texture bound, and most tiles carry only one or two
|
||||
std::vector<std::uint8_t> materials;
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -113,11 +121,8 @@ private:
|
||||
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 &Tile );
|
||||
void retire( resident_tile const &Tile );
|
||||
void build_indices();
|
||||
void build_palette();
|
||||
// resolves the cooked material names to the engine's materials, once
|
||||
void resolve_materials();
|
||||
void resolve_uniforms();
|
||||
void report_residency();
|
||||
|
||||
@@ -129,11 +134,6 @@ private:
|
||||
terrain_tile_loader m_loader;
|
||||
std::vector<terrain_tile_loader::request> m_wanted;
|
||||
std::vector<terrain_tile_loader::payload> m_arrived;
|
||||
// one tile within range, and the level its distance earns
|
||||
struct in_range {
|
||||
std::int64_t key;
|
||||
std::uint32_t level;
|
||||
};
|
||||
// what the last scan found in range, and the level wanted for each tile, to tell a useful
|
||||
// arrival from one the camera has already moved past
|
||||
std::vector<in_range> m_inrange;
|
||||
@@ -145,29 +145,28 @@ private:
|
||||
double m_finestwanted { 0.0 };
|
||||
double m_finest { 0.0 };
|
||||
std::unordered_map<std::int64_t, resident_tile> m_tiles;
|
||||
// one index buffer per mip level, over a grid of that level's sample count. vertices
|
||||
// are computed in the shader from gl_VertexID, so there is no vertex buffer at all.
|
||||
// held by pointer because a gl::buffer owns a name it cannot hand to a copy
|
||||
|
||||
// per level: the tiles' storage, the index buffer over that level's grid, and the
|
||||
// instances gathered for the draw. vertices are computed in the shader from gl_VertexID,
|
||||
// so there is no vertex buffer at all. held by pointer because the gl wrappers own names
|
||||
// they cannot hand to a copy
|
||||
std::vector<std::unique_ptr<terrain_tile_pool>> m_pools;
|
||||
std::vector<std::unique_ptr<gl::buffer>> m_indices;
|
||||
std::vector<std::uint32_t> m_indexcounts;
|
||||
std::vector<std::vector<float>> m_instances;
|
||||
// instance data for the draw being made, read by the vertex shader as a buffer texture
|
||||
std::optional<gl::buffer> m_instancebuffer;
|
||||
std::uint32_t m_instancetexture { 0 };
|
||||
terrain_ground m_ground;
|
||||
std::optional<gl::vao> m_vao;
|
||||
std::optional<gl::program> m_shader;
|
||||
// resolved once at link time; -1 for a uniform the compiler dropped, which gl ignores
|
||||
struct uniform_locations {
|
||||
int tileorigin { -1 }, samplestep { -1 };
|
||||
int heightbias { -1 }, heightscale { -1 }, nodata { -1 }, side { -1 }, morph { -1 };
|
||||
int heights { -1 }, materials { -1 }, palette { -1 };
|
||||
int groundtexture { -1 }, groundscale { -1 }, drawnmaterial { -1 }, tileworld { -1 };
|
||||
int hastexture { -1 }, fallback { -1 };
|
||||
int samplestep { -1 }, side { -1 }, morph { -1 };
|
||||
int heightbias { -1 }, heightscale { -1 }, nodata { -1 };
|
||||
int heights { -1 }, materials { -1 }, instances { -1 };
|
||||
int ground { -1 }, materialtable { -1 };
|
||||
} m_uniforms;
|
||||
std::vector<glm::vec3> m_palette;
|
||||
// one entry per cooked material: the engine texture to bind and how much ground one
|
||||
// repeat of it covers
|
||||
struct material_binding {
|
||||
texture_handle texture { null_handle };
|
||||
float scale { 4.f };
|
||||
};
|
||||
std::vector<material_binding> m_materials;
|
||||
|
||||
terrain_statistics m_stats;
|
||||
std::uint64_t m_frame { 0 };
|
||||
|
||||
240
rendering/terrainground.cpp
Normal file
240
rendering/terrainground.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
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 <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "glad/glad.h"
|
||||
#include "global_include/interfaces/ITexture_macros.h"
|
||||
|
||||
module eu07.rendering.terrainground;
|
||||
import eu07.gl.shader;
|
||||
import eu07.gl.vao;
|
||||
import eu07.glm;
|
||||
import eu07.utilities.logs;
|
||||
import eu07.rendering.renderer;
|
||||
import eu07.model.material;
|
||||
import eu07.model.texture;
|
||||
import eu07.global_include.interfaces.itexture;
|
||||
|
||||
namespace {
|
||||
|
||||
// a placeholder colour per material, spread around the hue circle so neighbouring materials
|
||||
// without a texture still read as different ground
|
||||
glm::vec3
|
||||
placeholder( std::size_t const Index ) {
|
||||
|
||||
auto const hue { static_cast<float>( Index ) * 0.61803399f };
|
||||
auto const phase { ( hue - std::floor( hue ) ) * 6.f };
|
||||
auto const rising { phase - std::floor( phase ) };
|
||||
switch( static_cast<int>( phase ) ) {
|
||||
case 0: return { 0.55f, 0.45f + 0.2f * rising, 0.3f };
|
||||
case 1: return { 0.55f - 0.2f * rising, 0.6f, 0.3f };
|
||||
case 2: return { 0.35f, 0.6f, 0.3f + 0.2f * rising };
|
||||
case 3: return { 0.35f, 0.6f - 0.15f * rising, 0.5f };
|
||||
case 4: return { 0.35f + 0.2f * rising, 0.45f, 0.5f };
|
||||
default: return { 0.55f, 0.45f, 0.5f - 0.15f * rising };
|
||||
}
|
||||
}
|
||||
|
||||
// gl state the copy pass changes, captured so it can be put back exactly
|
||||
struct saved_state {
|
||||
GLint drawframebuffer { 0 };
|
||||
GLint viewport[ 4 ] {};
|
||||
GLboolean depthtest { GL_FALSE };
|
||||
GLboolean blend { GL_FALSE };
|
||||
GLboolean cullface { GL_FALSE };
|
||||
GLboolean scissor { GL_FALSE };
|
||||
GLboolean srgb { GL_FALSE };
|
||||
|
||||
saved_state() {
|
||||
::glGetIntegerv( GL_DRAW_FRAMEBUFFER_BINDING, &drawframebuffer );
|
||||
::glGetIntegerv( GL_VIEWPORT, viewport );
|
||||
depthtest = ::glIsEnabled( GL_DEPTH_TEST );
|
||||
blend = ::glIsEnabled( GL_BLEND );
|
||||
cullface = ::glIsEnabled( GL_CULL_FACE );
|
||||
scissor = ::glIsEnabled( GL_SCISSOR_TEST );
|
||||
srgb = ::glIsEnabled( GL_FRAMEBUFFER_SRGB );
|
||||
}
|
||||
~saved_state() {
|
||||
auto const set = []( GLenum const Capability, GLboolean const Enabled ) {
|
||||
if( Enabled ) { ::glEnable( Capability ); } else { ::glDisable( Capability ); } };
|
||||
::glBindFramebuffer( GL_DRAW_FRAMEBUFFER, static_cast<GLuint>( drawframebuffer ) );
|
||||
::glViewport( viewport[ 0 ], viewport[ 1 ], viewport[ 2 ], viewport[ 3 ] );
|
||||
set( GL_DEPTH_TEST, depthtest );
|
||||
set( GL_BLEND, blend );
|
||||
set( GL_CULL_FACE, cullface );
|
||||
set( GL_SCISSOR_TEST, scissor );
|
||||
set( GL_FRAMEBUFFER_SRGB, srgb );
|
||||
}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
terrain_ground::~terrain_ground() {
|
||||
|
||||
destroy();
|
||||
}
|
||||
|
||||
bool
|
||||
terrain_ground::create( std::vector<std::string> const &Names, std::vector<float> const &Scales ) {
|
||||
|
||||
destroy();
|
||||
|
||||
try {
|
||||
gl::shader vertex( "terrainground.vert" );
|
||||
gl::shader fragment( "terrainground.frag" );
|
||||
m_copy.emplace( std::vector<std::reference_wrapper<gl::shader const>>( { vertex, fragment } ) );
|
||||
}
|
||||
catch( gl::shader_exception const &error ) {
|
||||
ErrorLog( std::string( "Terrain: ground copy shader failed, " ) + error.what() );
|
||||
return false;
|
||||
}
|
||||
m_vao.emplace();
|
||||
|
||||
std::size_t resolved { 0 };
|
||||
std::string unresolved;
|
||||
m_materials.resize( std::max<std::size_t>( 1, Names.size() ) );
|
||||
for( std::size_t index = 0; index < Names.size(); ++index ) {
|
||||
auto &entry { m_materials[ index ] };
|
||||
entry.fallback = placeholder( index );
|
||||
entry.scale = ( index < Scales.size() && Scales[ index ] >= 0.01f ) ? Scales[ index ] : 4.f;
|
||||
auto const handle { GfxRenderer->Fetch_Material( Names[ index ] ) };
|
||||
if( handle != null_handle ) {
|
||||
entry.texture = GfxRenderer->Material( handle )->GetTexture( 0 );
|
||||
}
|
||||
if( entry.texture != null_handle ) {
|
||||
++resolved;
|
||||
++m_pending;
|
||||
}
|
||||
else {
|
||||
// drawn in its placeholder colour instead, so a missing texture shows up as an
|
||||
// obvious flat patch rather than as black ground
|
||||
unresolved += ( unresolved.empty() ? "" : ", " ) + Names[ index ];
|
||||
}
|
||||
}
|
||||
WriteLog(
|
||||
"Terrain: " + std::to_string( resolved ) + " of " + std::to_string( Names.size() )
|
||||
+ " materials resolved to textures" );
|
||||
if( false == unresolved.empty() ) {
|
||||
WriteLog( "Terrain: no texture for " + unresolved );
|
||||
}
|
||||
|
||||
// a layer with its mip chain takes four thirds of its base level
|
||||
auto const layers { m_materials.size() };
|
||||
while( ( m_side > 64 ) && ( m_side * m_side * 4u * 4u / 3u * layers > budgetbytes ) ) { m_side /= 2; }
|
||||
auto const side { static_cast<GLsizei>( m_side ) };
|
||||
auto const levels { static_cast<GLint>( std::log2( m_side ) ) + 1 };
|
||||
|
||||
::glGenTextures( 1, &m_array );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, m_array );
|
||||
for( GLint level = 0; level < levels; ++level ) {
|
||||
::glTexImage3D(
|
||||
GL_TEXTURE_2D_ARRAY, level, GL_SRGB8_ALPHA8, std::max( 1, side >> level ), std::max( 1, side >> level ),
|
||||
static_cast<GLsizei>( layers ), 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr );
|
||||
}
|
||||
::glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
|
||||
::glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
|
||||
::glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT );
|
||||
::glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT );
|
||||
if( GLAD_GL_ARB_texture_filter_anisotropic ) {
|
||||
::glTexParameterf( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY, 16.f );
|
||||
}
|
||||
|
||||
::glGenTextures( 1, &m_table );
|
||||
::glBindTexture( GL_TEXTURE_2D, m_table );
|
||||
::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
|
||||
::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
|
||||
upload_table();
|
||||
|
||||
::glGenFramebuffers( 1, &m_framebuffer );
|
||||
opengl_texture::reset_unit_cache();
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
terrain_ground::destroy() {
|
||||
|
||||
if( m_array != 0 ) { ::glDeleteTextures( 1, &m_array ); m_array = 0; }
|
||||
if( m_table != 0 ) { ::glDeleteTextures( 1, &m_table ); m_table = 0; }
|
||||
if( m_framebuffer != 0 ) { ::glDeleteFramebuffers( 1, &m_framebuffer ); m_framebuffer = 0; }
|
||||
m_copy.reset();
|
||||
m_vao.reset();
|
||||
m_materials.clear();
|
||||
m_pending = 0;
|
||||
m_side = layerside;
|
||||
}
|
||||
|
||||
// rgb is the placeholder colour, alpha the metres per texture repeat - negative while the
|
||||
// material has no layer to sample yet
|
||||
void
|
||||
terrain_ground::upload_table() {
|
||||
|
||||
std::vector<float> texels;
|
||||
texels.reserve( m_materials.size() * 4 );
|
||||
for( auto const &entry : m_materials ) {
|
||||
texels.insert( texels.end(), { entry.fallback.r, entry.fallback.g, entry.fallback.b, entry.copied ? entry.scale : -entry.scale } );
|
||||
}
|
||||
::glBindTexture( GL_TEXTURE_2D, m_table );
|
||||
::glTexImage2D(
|
||||
GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast<GLsizei>( m_materials.size() ), 1, 0, GL_RGBA, GL_FLOAT, texels.data() );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_ground::update() {
|
||||
|
||||
if( ( m_pending == 0 ) || ( m_array == 0 ) ) { return; }
|
||||
|
||||
std::size_t copied { 0 };
|
||||
{
|
||||
saved_state const saved;
|
||||
::glDisable( GL_DEPTH_TEST );
|
||||
::glDisable( GL_BLEND );
|
||||
::glDisable( GL_CULL_FACE );
|
||||
::glDisable( GL_SCISSOR_TEST );
|
||||
// the sources are sRGB and sample as linear; writing through an sRGB target encodes
|
||||
// them back, so the layer keeps the precision the source had
|
||||
::glEnable( GL_FRAMEBUFFER_SRGB );
|
||||
::glBindFramebuffer( GL_DRAW_FRAMEBUFFER, m_framebuffer );
|
||||
::glViewport( 0, 0, static_cast<GLsizei>( m_side ), static_cast<GLsizei>( m_side ) );
|
||||
m_copy->bind();
|
||||
m_vao->bind();
|
||||
|
||||
for( std::size_t index = 0; index < m_materials.size(); ++index ) {
|
||||
auto &entry { m_materials[ index ] };
|
||||
if( ( true == entry.copied ) || ( entry.texture == null_handle ) ) { continue; }
|
||||
// binding asks the engine for the texture; it comes back unready while the data is
|
||||
// still loading, and the copy waits for a later frame
|
||||
GfxRenderer->Bind_Texture( 0, entry.texture );
|
||||
if( false == GfxRenderer->Texture( entry.texture ).get_is_ready() ) { continue; }
|
||||
|
||||
::glFramebufferTextureLayer( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_array, 0, static_cast<GLint>( index ) );
|
||||
::glDrawArrays( GL_TRIANGLES, 0, 3 );
|
||||
entry.copied = true;
|
||||
--m_pending;
|
||||
++copied;
|
||||
}
|
||||
|
||||
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 );
|
||||
upload_table();
|
||||
}
|
||||
}
|
||||
80
rendering/terrainground.cppm
Normal file
80
rendering/terrainground.cppm
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
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 <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "global_include/interfaces/ITexture_macros.h"
|
||||
|
||||
export module eu07.rendering.terrainground;
|
||||
import eu07.gl.shader;
|
||||
import eu07.gl.vao;
|
||||
import eu07.glm;
|
||||
import eu07.utilities.classes;
|
||||
|
||||
export {
|
||||
|
||||
// The ground textures of one heightfield, gathered into a single texture array so that a tile
|
||||
// of any number of materials is drawn in one call, and a table of what the shader needs per
|
||||
// material: the colour to use while there is no texture, and how many metres one repeat
|
||||
// covers.
|
||||
//
|
||||
// The scenery's textures are the engine's own - compressed, loaded in the background, in
|
||||
// whatever sizes their authors made them - so they cannot be blitted or copied into an array
|
||||
// directly. Each one is drawn into its layer by a small shader once the engine has it ready;
|
||||
// until then the material shows its placeholder colour. The layers are sRGB, as the sources
|
||||
// are.
|
||||
class terrain_ground {
|
||||
|
||||
public:
|
||||
terrain_ground() = default;
|
||||
~terrain_ground();
|
||||
terrain_ground( terrain_ground const & ) = delete;
|
||||
terrain_ground &operator=( terrain_ground const & ) = delete;
|
||||
|
||||
// resolves the cooked material names to the engine's textures and allocates the array.
|
||||
// returns false when the copy shader cannot be built
|
||||
bool create( std::vector<std::string> const &Names, std::vector<float> const &Scales );
|
||||
void destroy();
|
||||
// copies into the array every texture that has become ready since the last call. changes
|
||||
// gl state for the duration and puts back what it touched
|
||||
void update();
|
||||
|
||||
std::uint32_t textures() const { return m_array; }
|
||||
std::uint32_t table() const { return m_table; }
|
||||
|
||||
private:
|
||||
struct material {
|
||||
texture_handle texture { null_handle };
|
||||
bool copied { false };
|
||||
glm::vec3 fallback { 0.45f, 0.5f, 0.4f };
|
||||
float scale { 4.f };
|
||||
};
|
||||
|
||||
void upload_table();
|
||||
|
||||
// side of a layer, halved when many materials would make the array large
|
||||
static constexpr std::uint32_t layerside { 512 };
|
||||
static constexpr std::size_t budgetbytes { 96u << 20 };
|
||||
|
||||
std::vector<material> m_materials;
|
||||
std::uint32_t m_side { layerside };
|
||||
std::uint32_t m_array { 0 };
|
||||
std::uint32_t m_table { 0 };
|
||||
std::uint32_t m_framebuffer { 0 };
|
||||
std::size_t m_pending { 0 };
|
||||
std::optional<gl::program> m_copy;
|
||||
std::optional<gl::vao> m_vao;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -72,6 +72,7 @@ terrain_morph_begin( std::uint32_t const Level, double const Finest, double cons
|
||||
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 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
|
||||
|
||||
137
rendering/terraintilepool.cpp
Normal file
137
rendering/terraintilepool.cpp
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
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 <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "glad/glad.h"
|
||||
|
||||
module eu07.rendering.terraintilepool;
|
||||
import eu07.gl.buffer;
|
||||
|
||||
namespace {
|
||||
|
||||
// an array of integer samples. nothing may filter them: they carry the cooker's values,
|
||||
// no-data included
|
||||
GLuint
|
||||
create_array( GLenum const Internalformat, GLenum const Type, GLsizei const Side, GLsizei const Layers ) {
|
||||
|
||||
GLuint texture { 0 };
|
||||
::glGenTextures( 1, &texture );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, texture );
|
||||
::glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
|
||||
::glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
|
||||
::glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
|
||||
::glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
|
||||
// NOTE: glTexImage3D rather than glTexStorage3D, which is OpenGL 4.2; this renderer holds
|
||||
// a 3.3 context, where the entry point is not there at all
|
||||
::glTexImage3D( GL_TEXTURE_2D_ARRAY, 0, Internalformat, Side, Side, Layers, 0, GL_RED_INTEGER, Type, nullptr );
|
||||
return texture;
|
||||
}
|
||||
|
||||
// copies the first Layers layers of one integer array into another of the same side, on the
|
||||
// gpu, through a read framebuffer. the framebuffer bindings are put back as they were found,
|
||||
// so the renderer's own tracking of them stays true
|
||||
void
|
||||
copy_layers( GLuint const Source, GLuint const Target, GLsizei const Side, GLsizei const Layers ) {
|
||||
|
||||
GLint readbinding { 0 };
|
||||
::glGetIntegerv( GL_READ_FRAMEBUFFER_BINDING, &readbinding );
|
||||
GLuint framebuffer { 0 };
|
||||
::glGenFramebuffers( 1, &framebuffer );
|
||||
::glBindFramebuffer( GL_READ_FRAMEBUFFER, framebuffer );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, Target );
|
||||
for( GLsizei layer = 0; layer < Layers; ++layer ) {
|
||||
::glFramebufferTextureLayer( GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, Source, 0, layer );
|
||||
::glCopyTexSubImage3D( GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, 0, 0, Side, Side );
|
||||
}
|
||||
::glBindFramebuffer( GL_READ_FRAMEBUFFER, static_cast<GLuint>( readbinding ) );
|
||||
::glDeleteFramebuffers( 1, &framebuffer );
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
terrain_tile_pool::terrain_tile_pool( std::uint32_t const Side ) :
|
||||
m_side( Side ) {
|
||||
|
||||
m_unpack.emplace();
|
||||
}
|
||||
|
||||
terrain_tile_pool::~terrain_tile_pool() {
|
||||
|
||||
if( m_heights != 0 ) { ::glDeleteTextures( 1, &m_heights ); }
|
||||
if( m_materials != 0 ) { ::glDeleteTextures( 1, &m_materials ); }
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
terrain_tile_pool::acquire() {
|
||||
|
||||
if( true == m_free.empty() ) { grow(); }
|
||||
auto const slot { m_free.back() };
|
||||
m_free.pop_back();
|
||||
return slot;
|
||||
}
|
||||
|
||||
void
|
||||
terrain_tile_pool::release( std::uint32_t const Slot ) {
|
||||
|
||||
m_free.push_back( Slot );
|
||||
}
|
||||
|
||||
void
|
||||
terrain_tile_pool::grow() {
|
||||
|
||||
auto const capacity { m_capacity == 0 ? initialcapacity : m_capacity * 2 };
|
||||
auto const side { static_cast<GLsizei>( m_side ) };
|
||||
|
||||
auto const heights { create_array( GL_R16UI, GL_UNSIGNED_SHORT, side, static_cast<GLsizei>( capacity ) ) };
|
||||
auto const materials { create_array( GL_R8UI, GL_UNSIGNED_BYTE, side, static_cast<GLsizei>( capacity ) ) };
|
||||
if( m_capacity > 0 ) {
|
||||
copy_layers( m_heights, heights, side, static_cast<GLsizei>( m_capacity ) );
|
||||
copy_layers( m_materials, materials, side, static_cast<GLsizei>( m_capacity ) );
|
||||
::glDeleteTextures( 1, &m_heights );
|
||||
::glDeleteTextures( 1, &m_materials );
|
||||
}
|
||||
m_heights = heights;
|
||||
m_materials = materials;
|
||||
|
||||
// new slots are handed out lowest first
|
||||
for( auto slot { capacity }; slot > m_capacity; --slot ) {
|
||||
m_free.push_back( slot - 1 );
|
||||
}
|
||||
m_capacity = capacity;
|
||||
}
|
||||
|
||||
void
|
||||
terrain_tile_pool::upload( std::uint32_t const Slot, std::uint16_t const *Heights, std::uint8_t const *Materials ) {
|
||||
|
||||
auto const samples { static_cast<std::size_t>( m_side ) * m_side };
|
||||
auto const heightbytes { samples * sizeof( std::uint16_t ) };
|
||||
|
||||
// orphaned and refilled per tile: the driver can hand out fresh storage instead of waiting
|
||||
// for the previous upload to finish
|
||||
m_unpack->bind( gl::buffer::PIXEL_UNPACK_BUFFER );
|
||||
m_unpack->allocate( gl::buffer::PIXEL_UNPACK_BUFFER, static_cast<GLsizeiptr>( heightbytes + samples ), GL_STREAM_DRAW );
|
||||
m_unpack->upload( gl::buffer::PIXEL_UNPACK_BUFFER, Heights, 0, static_cast<GLsizeiptr>( heightbytes ) );
|
||||
m_unpack->upload( gl::buffer::PIXEL_UNPACK_BUFFER, Materials, static_cast<int>( heightbytes ), static_cast<GLsizeiptr>( samples ) );
|
||||
|
||||
auto const side { static_cast<GLsizei>( m_side ) };
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, m_heights );
|
||||
::glTexSubImage3D(
|
||||
GL_TEXTURE_2D_ARRAY, 0, 0, 0, static_cast<GLint>( Slot ), side, side, 1,
|
||||
GL_RED_INTEGER, GL_UNSIGNED_SHORT, nullptr );
|
||||
::glBindTexture( GL_TEXTURE_2D_ARRAY, m_materials );
|
||||
::glTexSubImage3D(
|
||||
GL_TEXTURE_2D_ARRAY, 0, 0, 0, static_cast<GLint>( Slot ), side, side, 1,
|
||||
GL_RED_INTEGER, GL_UNSIGNED_BYTE, reinterpret_cast<void const *>( heightbytes ) );
|
||||
gl::buffer::unbind( gl::buffer::PIXEL_UNPACK_BUFFER );
|
||||
}
|
||||
62
rendering/terraintilepool.cppm
Normal file
62
rendering/terraintilepool.cppm
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 <optional>
|
||||
#include <vector>
|
||||
|
||||
export module eu07.rendering.terraintilepool;
|
||||
import eu07.gl.buffer;
|
||||
|
||||
export {
|
||||
|
||||
// GPU storage for the terrain tiles of one mip level: a texture array of heights and one of
|
||||
// material indices, a layer per tile. Tiles of a level all have the same side, so they share
|
||||
// the arrays, and the whole level draws from them in a single call.
|
||||
//
|
||||
// Slots are handed out and taken back as tiles come and go. The arrays start small and double
|
||||
// when full, copying what they hold on the gpu, so a level only ever takes the memory its
|
||||
// tiles need. Uploads go through a pixel buffer that is orphaned for each tile, which lets the
|
||||
// driver take the data without stalling on the previous transfer.
|
||||
class terrain_tile_pool {
|
||||
|
||||
public:
|
||||
explicit terrain_tile_pool( std::uint32_t const Side );
|
||||
~terrain_tile_pool();
|
||||
terrain_tile_pool( terrain_tile_pool const & ) = delete;
|
||||
terrain_tile_pool &operator=( terrain_tile_pool const & ) = delete;
|
||||
|
||||
// a free layer, growing the arrays when there is none
|
||||
std::uint32_t acquire();
|
||||
void release( std::uint32_t const Slot );
|
||||
// writes one tile's samples into its layer. both arrays hold Side * Side values
|
||||
void upload( std::uint32_t const Slot, std::uint16_t const *Heights, std::uint8_t const *Materials );
|
||||
|
||||
std::uint32_t heights() const { return m_heights; }
|
||||
std::uint32_t materials() const { return m_materials; }
|
||||
std::uint32_t side() const { return m_side; }
|
||||
// gpu memory the arrays occupy
|
||||
std::size_t bytes() const { return static_cast<std::size_t>( m_side ) * m_side * 3u * m_capacity; }
|
||||
|
||||
private:
|
||||
void grow();
|
||||
|
||||
static constexpr std::uint32_t initialcapacity { 16 };
|
||||
|
||||
std::uint32_t m_side { 0 };
|
||||
std::uint32_t m_capacity { 0 };
|
||||
std::uint32_t m_heights { 0 };
|
||||
std::uint32_t m_materials { 0 };
|
||||
std::optional<gl::buffer> m_unpack;
|
||||
std::vector<std::uint32_t> m_free;
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user