diff --git a/CMakeLists.txt b/CMakeLists.txt index 62498027..c0792952 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,8 @@ set(SOURCES "application/shotrunner.cpp" "rendering/terrainclipmap.cpp" "rendering/terraintileloader.cpp" +"rendering/terraintilepool.cpp" +"rendering/terrainground.cpp" "simulation/loadprofile.cpp" "simulation/logiccook.cpp" "simulation/terrainbake.cpp" diff --git a/application/driveruipanels.cpp b/application/driveruipanels.cpp index 579b9619..f138de24 100644 --- a/application/driveruipanels.cpp +++ b/application/driveruipanels.cpp @@ -1516,7 +1516,8 @@ debug_panel::update_section_renderer( std::vector &Output ) { + ", loaded out to " + to_string( field.range, 0 ) + " m", Global.UITextColor ); Output.emplace_back( - " tiles: " + std::to_string( stats.inview ) + " in view, " + " tiles: " + std::to_string( stats.inview ) + " in range, " + + std::to_string( stats.drawn ) + " drawn, " + std::to_string( stats.resident ) + " on gpu (" + std::to_string( stats.texturebytes / 1048576 ) + " MB), " + std::to_string( stats.wanted ) + " wanted, " + std::to_string( field.backlog ) + " in loader, " + std::to_string( stats.uploaded ) + " uploaded, " + std::to_string( stats.dropped ) + " dropped stale", diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 4348a156..bf5a7234 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -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) diff --git a/rendering/terrainclipmap.cpp b/rendering/terrainclipmap.cpp index f193e03b..646036b2 100644 --- a/rendering/terrainclipmap.cpp +++ b/rendering/terrainclipmap.cpp @@ -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( 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( *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( 1, m_reader.materials().size() ) ); - for( std::size_t index = 0; index < m_reader.materials().size(); ++index ) { - auto const hue { static_cast( 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( 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( side ), Tile.heights.data() ); - tile.materialtexture = create_texture( - GL_R8UI, GL_UNSIGNED_BYTE, static_cast( 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 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( 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( 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( 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( 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( 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( 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( tile.x * tilesize - Viewpoint.x ), + static_cast( -Viewpoint.y ) - m_depthbias, + static_cast( tile.z * tilesize - Viewpoint.z ), + static_cast( tile.slot ), + static_cast( tile.x * tilesize ), + static_cast( 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( 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( 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( instances.size() * sizeof( float ) ), GL_STREAM_DRAW ); + m_instancebuffer->upload( + gl::buffer::TEXTURE_BUFFER, instances.data(), 0, static_cast( instances.size() * sizeof( float ) ) ); + ::glActiveTexture( GL_TEXTURE4 ); + ::glBindTexture( GL_TEXTURE_BUFFER, m_instancetexture ); + ::glTexBuffer( GL_TEXTURE_BUFFER, GL_RGBA32F, static_cast( *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( tile.x * tilesize - Viewpoint.x ), - static_cast( -Viewpoint.y ) - m_depthbias, - static_cast( tile.z * tilesize - Viewpoint.z ) }; - ::glUniform3fv( m_uniforms.tileorigin, 1, &origin.x ); - ::glUniform1f( m_uniforms.samplestep, gridstep * static_cast( 1u << tile.level ) ); - ::glUniform1i( m_uniforms.side, static_cast( 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( 1u << level ) ); + ::glUniform1i( m_uniforms.side, static_cast( 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( terrain_morph_begin( tile.level, m_finest, tilesize ) ), - static_cast( terrain_morph_end( tile.level, m_finest, tilesize ) ) ); + static_cast( terrain_morph_begin( level, m_finest, tilesize ) ), + static_cast( 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( tile.x * tilesize ), static_cast( 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( material, m_palette.size() - 1 ) ] }; - ::glUniform3fv( m_uniforms.fallback, 1, &fallback.x ); - - ::glDrawElements( - GL_TRIANGLES, static_cast( m_indexcounts[ tile.level ] ), GL_UNSIGNED_INT, nullptr ); - } + m_indices[ level ]->bind( gl::buffer::ELEMENT_ARRAY_BUFFER ); + ::glDrawElementsInstanced( + GL_TRIANGLES, static_cast( m_indexcounts[ level ] ), GL_UNSIGNED_INT, nullptr, + static_cast( 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 ); } diff --git a/rendering/terrainclipmap.cppm b/rendering/terrainclipmap.cppm index 9a6f4f92..3fa163bf 100644 --- a/rendering/terrainclipmap.cppm +++ b/rendering/terrainclipmap.cppm @@ -10,13 +10,13 @@ http://mozilla.org/MPL/2.0/. module; #include #include +#include #include #include #include #include #include -#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; + 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 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 m_wanted; std::vector 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 m_inrange; @@ -145,29 +145,28 @@ private: double m_finestwanted { 0.0 }; double m_finest { 0.0 }; std::unordered_map 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> m_pools; std::vector> m_indices; std::vector m_indexcounts; + std::vector> m_instances; + // instance data for the draw being made, read by the vertex shader as a buffer texture + std::optional m_instancebuffer; + std::uint32_t m_instancetexture { 0 }; + terrain_ground m_ground; std::optional m_vao; std::optional 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 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 m_materials; terrain_statistics m_stats; std::uint64_t m_frame { 0 }; diff --git a/rendering/terrainground.cpp b/rendering/terrainground.cpp new file mode 100644 index 00000000..b3ba4cff --- /dev/null +++ b/rendering/terrainground.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +#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( Index ) * 0.61803399f }; + auto const phase { ( hue - std::floor( hue ) ) * 6.f }; + auto const rising { phase - std::floor( phase ) }; + switch( static_cast( 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( 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 const &Names, std::vector const &Scales ) { + + destroy(); + + try { + gl::shader vertex( "terrainground.vert" ); + gl::shader fragment( "terrainground.frag" ); + m_copy.emplace( std::vector>( { 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( 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( m_side ) }; + auto const levels { static_cast( 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( 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 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( 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( m_side ), static_cast( 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( 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(); + } +} diff --git a/rendering/terrainground.cppm b/rendering/terrainground.cppm new file mode 100644 index 00000000..ae913eb2 --- /dev/null +++ b/rendering/terrainground.cppm @@ -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 +#include +#include +#include +#include + +#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 const &Names, std::vector 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 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 m_copy; + std::optional m_vao; +}; + +} diff --git a/rendering/terrainstatus.cppm b/rendering/terrainstatus.cppm index 9e886c33..9b7db39f 100644 --- a/rendering/terrainstatus.cppm +++ b/rendering/terrainstatus.cppm @@ -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 diff --git a/rendering/terraintilepool.cpp b/rendering/terraintilepool.cpp new file mode 100644 index 00000000..82337d0c --- /dev/null +++ b/rendering/terraintilepool.cpp @@ -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 +#include +#include +#include + +#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( 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( m_side ) }; + + auto const heights { create_array( GL_R16UI, GL_UNSIGNED_SHORT, side, static_cast( capacity ) ) }; + auto const materials { create_array( GL_R8UI, GL_UNSIGNED_BYTE, side, static_cast( capacity ) ) }; + if( m_capacity > 0 ) { + copy_layers( m_heights, heights, side, static_cast( m_capacity ) ); + copy_layers( m_materials, materials, side, static_cast( 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( 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( heightbytes + samples ), GL_STREAM_DRAW ); + m_unpack->upload( gl::buffer::PIXEL_UNPACK_BUFFER, Heights, 0, static_cast( heightbytes ) ); + m_unpack->upload( gl::buffer::PIXEL_UNPACK_BUFFER, Materials, static_cast( heightbytes ), static_cast( samples ) ); + + auto const side { static_cast( m_side ) }; + ::glBindTexture( GL_TEXTURE_2D_ARRAY, m_heights ); + ::glTexSubImage3D( + GL_TEXTURE_2D_ARRAY, 0, 0, 0, static_cast( 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( Slot ), side, side, 1, + GL_RED_INTEGER, GL_UNSIGNED_BYTE, reinterpret_cast( heightbytes ) ); + gl::buffer::unbind( gl::buffer::PIXEL_UNPACK_BUFFER ); +} diff --git a/rendering/terraintilepool.cppm b/rendering/terraintilepool.cppm new file mode 100644 index 00000000..e1af1079 --- /dev/null +++ b/rendering/terraintilepool.cppm @@ -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 +#include +#include +#include + +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( 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 m_unpack; + std::vector m_free; +}; + +} diff --git a/shaders/terrainclipmap.frag b/shaders/terrainclipmap.frag index 4a89041e..a3b30cc4 100644 --- a/shaders/terrainclipmap.frag +++ b/shaders/terrainclipmap.frag @@ -1,44 +1,75 @@ // Terrain heightfield, fragment stage. // -// A tile is drawn once per material it contains, with that material's texture bound, and -// every fragment belonging to a different material is dropped. Texture coordinates are -// world space divided by the tiling the cooker measured from the source geometry, so the -// ground keeps the scale the scenery was authored with. +// Every material of a tile is drawn in the same pass. The fragment finds the four samples +// around it in the tile's layer, drops itself if any of them carries no terrain - the +// heightfield only has ground where a whole cell does - and blends the ground textures of their +// materials by distance, so that material boundaries follow the samples smoothly rather than +// stepping. Texture coordinates are world space divided by the tiling the cooker measured from +// the source geometry, so the ground keeps the scale the scenery was authored with. in vec4 f_pos; in vec3 f_normal; in vec2 f_ground; -in float f_valid; -flat in uint f_material; +flat in vec2 f_tileworld; +flat in int f_layer; #include #include #include -uniform sampler2D ground; -uniform float groundscale; // metres of ground covered by one texture repeat -uniform uint drawnmaterial; // the material this pass is drawing -uniform bool hastexture; // false when the material has no texture to sample -uniform vec3 fallback; // colour used in that case +uniform usampler2DArray heights; +uniform usampler2DArray materials; +uniform sampler2DArray ground; +// per material: rgb the colour to show while it has no texture, a the metres one texture +// repeat covers - negative while there is no texture layer for it yet +uniform sampler2D materialtable; +uniform float samplestep; +uniform int side; +uniform uint nodata; layout(location = 0) out vec4 out_color; #if MOTIONBLUR_ENABLED layout(location = 1) out vec4 out_motion; #endif +vec3 material_colour( uint Material ) +{ + vec4 entry = texelFetch( materialtable, ivec2( int( Material ), 0 ), 0 ); + if( entry.a < 0.0 ) { return entry.rgb; } + return texture( ground, vec3( f_ground / max( entry.a, 0.01 ), float( Material ) ) ).rgb; +} + void main() { - // f_valid is 1.0 only where every vertex of this triangle carried terrain, so a - // triangle reaching into a hole disappears. it erodes the boundary by up to one - // sample, which is the price of holes being a property of samples, not of triangles - if( f_valid < 0.999 ) { discard; } - // this pass belongs to one material; everything else is drawn by another pass - if( f_material != drawnmaterial ) { discard; } + vec2 cell = ( f_ground - f_tileworld ) / samplestep; + ivec2 base = clamp( ivec2( floor( cell ) ), ivec2( 0 ), ivec2( side - 2 ) ); + vec2 weight = clamp( cell - vec2( base ), 0.0, 1.0 ); + + ivec2 corners[ 4 ] = ivec2[ 4 ]( base, base + ivec2( 1, 0 ), base + ivec2( 0, 1 ), base + ivec2( 1, 1 ) ); + float weights[ 4 ] = float[ 4 ]( + ( 1.0 - weight.x ) * ( 1.0 - weight.y ), + weight.x * ( 1.0 - weight.y ), + ( 1.0 - weight.x ) * weight.y, + weight.x * weight.y ); + + uint kinds[ 4 ]; + for( int index = 0; index < 4; ++index ) { + if( texelFetch( heights, ivec3( corners[ index ], f_layer ), 0 ).r == nodata ) { discard; } + kinds[ index ] = texelFetch( materials, ivec3( corners[ index ], f_layer ), 0 ).r; + } + + vec3 albedo; + if( ( kinds[ 0 ] == kinds[ 1 ] ) && ( kinds[ 0 ] == kinds[ 2 ] ) && ( kinds[ 0 ] == kinds[ 3 ] ) ) { + // the common case: one material across the whole cell, one texture sample + albedo = material_colour( kinds[ 0 ] ); + } + else { + albedo = vec3( 0.0 ); + for( int index = 0; index < 4; ++index ) { + albedo += weights[ index ] * material_colour( kinds[ index ] ); + } + } - vec3 albedo = ( - hastexture ? - texture( ground, f_ground / max( groundscale, 0.01 ) ).rgb : - fallback ); // a plain lambert term against the first directional light, so that relief reads float lambert = 0.35; if( lights_count >= 1U && lights[ 0 ].type == LIGHT_DIR ) { diff --git a/shaders/terrainclipmap.vert b/shaders/terrainclipmap.vert index cc04019d..65d4bf5f 100644 --- a/shaders/terrainclipmap.vert +++ b/shaders/terrainclipmap.vert @@ -1,105 +1,103 @@ // Terrain heightfield, vertex stage. // -// There is no vertex buffer: the grid exists only as an index buffer, and a sample's -// row and column come from gl_VertexID. Heights are read as unsigned integers so that -// the cooker's no-data value survives unchanged instead of being normalised into a float. +// There is no vertex buffer: the grid exists only as an index buffer, and a sample's row and +// column come from gl_VertexID. A whole level is drawn in one instanced call; what differs +// between its tiles - where each stands and which layer of the level's texture arrays holds +// it - comes from a buffer texture, two texels per instance. Heights are read as unsigned +// integers so that the cooker's no-data value survives unchanged instead of being normalised +// into a float. // // Levels meet without cracks and change without popping because each vertex morphs into the -// next coarser level as its distance nears the end of its level's range (CDLOD). The mip -// chain is point sampled, so the next level's grid is this level's even samples: an odd -// vertex slides onto its even neighbour and takes its height, and by the end of the range -// the tile is, vertex for vertex, the coarser tile that lies beyond it. +// next coarser level as its distance nears the end of its level's range (CDLOD). The mip chain +// is point sampled, so the next level's grid is this level's even samples: an odd vertex +// slides onto its even neighbour and takes its height, and by the end of the range the tile +// is, vertex for vertex, the coarser tile that lies beyond it. #include -uniform usampler2D heights; -uniform usampler2D materials; +uniform usampler2DArray heights; +uniform samplerBuffer instances; -// tile origin relative to the viewpoint, with the viewpoint height already folded into y -uniform vec3 tileorigin; -uniform float samplestep; // metres between samples at the level this tile is drawn at +uniform float samplestep; // metres between samples at this level uniform float heightbias; // world height of stored value 0 uniform float heightscale; // metres per stored unit uniform uint nodata; uniform int side; // samples along a tile side at this level uniform vec2 morph; // distance at which morphing into the next level starts, and where it is complete -uniform vec2 tileworld; // tile origin in world space, for texture coordinates out vec4 f_pos; out vec3 f_normal; -out vec2 f_ground; -out float f_valid; -flat out uint f_material; +out vec2 f_ground; // world position on the ground, for texturing and material lookup +flat out vec2 f_tileworld; // world position of the tile's first sample +flat out int f_layer; // the tile's layer in the level's arrays -// height of a neighbouring sample, for the central difference the normal is built from. -// a neighbour that carries no terrain would otherwise decode as the no-data value and -// read as a surface a hundred metres up, wrecking the normal and leaving the ground -// unlit; Fallback keeps the difference at zero there, so the edge of a hole comes out -// flat rather than black -// height of the first neighbouring sample that carries terrain, or the lowest height on -// the map when the sample is surrounded by emptiness +int layer; + +// height of the first neighbouring sample that carries terrain, or the lowest height on the +// map when the sample is surrounded by emptiness. a sample outside the cooked ground has no +// height of its own, and decoding the no-data value as one would put the vertex at the top of +// the map's range float borrowed_height( ivec2 Texel ) { const ivec2 offsets[ 4 ] = ivec2[ 4 ]( ivec2( -1, 0 ), ivec2( 1, 0 ), ivec2( 0, -1 ), ivec2( 0, 1 ) ); for( int index = 0; index < 4; ++index ) { ivec2 neighbour = clamp( Texel + offsets[ index ], ivec2( 0 ), ivec2( side - 1 ) ); - uint raw = texelFetch( heights, neighbour, 0 ).r; + uint raw = texelFetch( heights, ivec3( neighbour, layer ), 0 ).r; if( raw != nodata ) { return heightbias + float( raw ) * heightscale; } } return heightbias; } +float height_at( ivec2 Texel ) +{ + uint raw = texelFetch( heights, ivec3( Texel, layer ), 0 ).r; + return ( raw == nodata ) ? borrowed_height( Texel ) : heightbias + float( raw ) * heightscale; +} + +// height of a neighbouring sample, for the central difference the normal is built from. a +// neighbour carrying no terrain gives Fallback, so the edge of a hole comes out flat rather +// than as a wall to a surface a hundred metres up float sample_height( ivec2 Texel, float Fallback ) { ivec2 clamped = clamp( Texel, ivec2( 0 ), ivec2( side - 1 ) ); - uint raw = texelFetch( heights, clamped, 0 ).r; + uint raw = texelFetch( heights, ivec3( clamped, layer ), 0 ).r; if( raw == nodata ) { return Fallback; } return heightbias + float( raw ) * heightscale; } void main() { - int column = gl_VertexID % side; - int row = gl_VertexID / side; - ivec2 texel = ivec2( column, row ); + vec4 placement = texelFetch( instances, gl_InstanceID * 2 ); + vec4 world = texelFetch( instances, gl_InstanceID * 2 + 1 ); + vec3 tileorigin = placement.xyz; + layer = int( placement.w ); - uint raw = texelFetch( heights, texel, 0 ).r; - f_valid = ( raw == nodata ) ? 0.0 : 1.0; - f_material = texelFetch( materials, texel, 0 ).r; - - // a sample outside the cooked ground has no height. decoding the no-data value as - // one puts the vertex at the top of the map's range, and since f_valid is - // interpolated, the slivers of triangle nearest the valid corners survive the - // fragment test and stretch up there. so an empty sample borrows a neighbour's - // height: the fragments are still dropped, but the geometry stays on the ground - float height = heightbias + float( raw ) * heightscale; - if( raw == nodata ) { - height = borrowed_height( texel ); - } - vec3 local = tileorigin + vec3( float( column ) * samplestep, height, float( row ) * samplestep ); + ivec2 texel = ivec2( gl_VertexID % side, gl_VertexID / side ); + float height = height_at( texel ); + vec3 local = tileorigin + vec3( vec2( texel ).x * samplestep, height, vec2( texel ).y * samplestep ); // the viewpoint is at the horizontal origin, so the distance is the length of local.xz, // measured as the renderer measures it when choosing the level float morphing = clamp( ( length( local.xz ) - morph.x ) / ( morph.y - morph.x ), 0.0, 1.0 ); if( morphing > 0.0 ) { ivec2 coarse = texel - ( texel % 2 ); - uint coarseraw = texelFetch( heights, coarse, 0 ).r; - float coarseheight = ( coarseraw == nodata ) ? borrowed_height( coarse ) : heightbias + float( coarseraw ) * heightscale; vec2 grid = mix( vec2( texel ), vec2( coarse ), morphing ); - local = tileorigin + vec3( grid.x * samplestep, mix( height, coarseheight, morphing ), grid.y * samplestep ); + local = tileorigin + vec3( grid.x * samplestep, mix( height, height_at( coarse ), morphing ), grid.y * samplestep ); } - // central differences over the neighbouring samples; at a tile edge the clamp repeats - // the border sample, which is what the neighbouring tile holds there as well + // central differences over the neighbouring samples; at a tile edge the clamp repeats the + // border sample, which is what the neighbouring tile holds there as well float west = sample_height( texel - ivec2( 1, 0 ), height ); float east = sample_height( texel + ivec2( 1, 0 ), height ); float north = sample_height( texel - ivec2( 0, 1 ), height ); float south = sample_height( texel + ivec2( 0, 1 ), height ); vec3 normal = normalize( vec3( west - east, 2.0 * samplestep, north - south ) ); - // world coordinates, so the ground texture stays put as the camera moves and as the - // tile changes mip level - f_ground = tileworld + ( local.xz - tileorigin.xz ); + f_tileworld = world.xy; + f_layer = layer; + // world coordinates, so the ground texture stays put as the camera moves and as the tile + // changes level + f_ground = f_tileworld + ( local.xz - tileorigin.xz ); f_pos = modelview * vec4( local, 1.0 ); f_normal = normalize( modelviewnormal * normal ); diff --git a/shaders/terrainground.frag b/shaders/terrainground.frag new file mode 100644 index 00000000..b2767702 --- /dev/null +++ b/shaders/terrainground.frag @@ -0,0 +1,13 @@ +// Terrain ground textures, copy pass. The source is sampled with its own mip chain, so a +// texture larger than the layer is filtered down rather than point sampled. + +in vec2 f_coord; + +uniform sampler2D source; + +layout(location = 0) out vec4 out_color; + +void main() +{ + out_color = vec4( texture( source, f_coord ).rgb, 1.0 ); +} diff --git a/shaders/terrainground.vert b/shaders/terrainground.vert new file mode 100644 index 00000000..e2800e1c --- /dev/null +++ b/shaders/terrainground.vert @@ -0,0 +1,11 @@ +// Terrain ground textures, copy pass: a triangle covering the whole target, with no vertex +// buffer. Draws one source texture into one layer of the ground array. + +out vec2 f_coord; + +void main() +{ + vec2 corner = vec2( float( ( gl_VertexID & 1 ) << 2 ), float( ( gl_VertexID & 2 ) << 1 ) ); + f_coord = corner * 0.5; + gl_Position = vec4( corner - 1.0, 0.0, 1.0 ); +}