17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-05 08:44:32 +01:00

Draw terrain one call per level, from texture arrays, culled to the view

Each resident tile had a height texture and a material texture of its own, uploaded with
glTexImage2D, and was drawn once per material it held with every other material's
fragments discarded, all of them whether in view or not. Thousands of draws a frame.

Now the tiles of a level live in one pair of texture arrays (terrain_tile_pool), a layer
per tile. A pool starts at sixteen layers and doubles when full, copying what it holds on
the gpu, so a level takes only the memory its tiles need; uploads go through a pixel
buffer orphaned for each tile. The ground textures of every material are gathered into
one sRGB array with mipmaps (terrain_ground) - drawn into it layer by layer by a small
shader once the engine has each texture ready, since the scenery's textures are
compressed and cannot be blitted - with a table of placeholder colours and measured
tiling beside it.

Each frame the tiles in range are tested against the frustum by bounding sphere and
gathered per level into a buffer texture of instance data, and each level is drawn in
one instanced call. The fragment shader reads the four samples around it, drops the cell
when any of them carries no ground - exactly the rule the bake already assumed - and
blends the ground textures of their materials by distance, so material boundaries
follow the samples smoothly instead of stepping along triangle edges.

State the copy pass changes is captured and put back, and the texture unit cache is
reset after raw binds, so the rest of the renderer finds things as it left them.

On baltyk: all 30 materials land in the array, 663 of the 1120 tiles in range are drawn
after culling, and a frame is at most six terrain draws. The residency line in the log
now says how many tiles in range were drawn.
This commit is contained in:
maj00r
2026-09-17 16:28:43 +02:00
parent be4d691ae2
commit d2a5f284e3
14 changed files with 808 additions and 303 deletions

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