mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-31 12:19:06 +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:
@@ -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