17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-02 20:56:27 +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,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 );