mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 07:29:20 +02: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:
@@ -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"
|
||||
|
||||
@@ -1516,7 +1516,8 @@ debug_panel::update_section_renderer( std::vector<text_line> &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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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 <common>
|
||||
#include <apply_fog.glsl>
|
||||
#include <tonemapping.glsl>
|
||||
|
||||
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 ) {
|
||||
|
||||
@@ -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 <common>
|
||||
|
||||
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 );
|
||||
|
||||
13
shaders/terrainground.frag
Normal file
13
shaders/terrainground.frag
Normal file
@@ -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 );
|
||||
}
|
||||
11
shaders/terrainground.vert
Normal file
11
shaders/terrainground.vert
Normal file
@@ -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 );
|
||||
}
|
||||
Reference in New Issue
Block a user