17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-18 00:59:18 +02:00
Files
maszyna/rendering/terrainground.cpp
maj00r d2a5f284e3 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.
2026-09-17 16:28:43 +02:00

241 lines
8.8 KiB
C++

/*
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();
}
}