17
0
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:
maj00r
2026-09-17 16:28:43 +02:00
parent be4d691ae2
commit d2a5f284e3
14 changed files with 808 additions and 303 deletions

View File

@@ -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"

View File

@@ -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",

View File

@@ -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)

View File

@@ -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 );
}

View File

@@ -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
View 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();
}
}

View 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;
};
}

View File

@@ -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

View 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 );
}

View 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;
};
}

View File

@@ -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 ) {

View File

@@ -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 );

View 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 );
}

View 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 );
}