Terrain Fixes - Seamsless & Optimizations (#3442)

* Fix terrain seams and optimize

Overlap LODs by one step to fix holes in LOD transitions
Reuse vertices that exist on same key when building diamond square

https://files.facepunch.com/sampavlovic/1b2411b1/8mb.video-eW2-tNb22a60.mp4

* Add NoTile class and make terrain use it
https://files.facepunch.com/sampavlovic/1b2411b1/sbox-dev_R1FwUmLhvu.mp4
https://files.facepunch.com/sampavlovic/1b2411b1/sbox-dev_YhKyIwvhve.mp4

* Sure why not Mr. Robot
This commit is contained in:
Sam Pavlovic
2025-11-26 12:49:41 -03:00
committed by GitHub
parent 09d20a4482
commit b0e3a8d964
3 changed files with 101 additions and 73 deletions

View File

@@ -36,12 +36,13 @@ internal static class TerrainClipmap
int pad = 1;
int radius = step * (g + pad);
int innerRadius = (g * prevStep) - prevStep; // Overlap by one step
for ( int y = -radius; y < radius; y += step )
{
for ( int x = -radius; x < radius; x += step )
{
if ( Math.Max( Math.Abs( x + prevStep ), Math.Abs( y + prevStep ) ) < (g * prevStep) )
if ( Math.Max( Math.Abs( x + prevStep ), Math.Abs( y + prevStep ) ) < innerRadius )
continue;
vertices.Add( new PosAndLodVertex( new Vector3( x, y, level ) ) );
@@ -66,18 +67,27 @@ internal static class TerrainClipmap
}
/// <summary>
/// Inefficient implementation of diamond square, it's not merging verticies.
/// Diamond-square implementation trying to reduce duplicate vertices.
/// </summary>
/// <returns></returns>
public static Mesh GenerateMesh_DiamondSquare( int LodLevels, int LodExtentTexels, Material material, int subdivisionFactor = 1, int subdivisionLodCount = 3 )
{
var total = LodLevels * 36 * (LodExtentTexels / 2 + 1) * (LodExtentTexels / 2 + 1) * subdivisionFactor * subdivisionFactor;
var vertices = ArrayPool<PosAndLodVertex>.Shared.Rent( total );
var indices = ArrayPool<int>.Shared.Rent( total * 3 );
var vertexMap = new Dictionary<(float x, float y, int lod), int>( total );
var vertices = new List<PosAndLodVertex>( total );
var indices = new List<int>( total * 3 );
int vertex = 0;
int idx = 0;
int GetOrAddVertex( float x, float y, int level )
{
var key = (x, y, level);
if ( !vertexMap.TryGetValue( key, out int index ) )
{
index = vertices.Count;
vertices.Add( new PosAndLodVertex( new Vector3( x, y, level ) ) );
vertexMap[key] = index;
}
return index;
}
// Loop through each LOD level
for ( int level = 0; level < LodLevels; level++ )
@@ -93,7 +103,7 @@ internal static class TerrainClipmap
int radius = lodBaseStep * (g + pad);
int prevLodBaseStep = level > 0 ? (1 << (level - 1)) : 0;
int innerRadius = prevLodBaseStep * g;
int innerRadius = (prevLodBaseStep * g) - prevLodBaseStep; // Overlap by one step
for ( float y = -radius; y < radius; y += step )
{
@@ -110,112 +120,100 @@ internal static class TerrainClipmap
// | / | \ |
// G-----H-----I
var vecA = new Vector3( x, y, level );
var vecC = new Vector3( x + step, y, level );
var vecG = new Vector3( x, y + step, level );
var vecI = new Vector3( x + step, y + step, level );
var vecB = (vecA + vecC) * 0.5f;
var vecD = (vecA + vecG) * 0.5f;
var vecF = (vecC + vecI) * 0.5f;
var vecH = (vecG + vecI) * 0.5f;
var vecE = (vecA + vecI) * 0.5f;
vertices[vertex++].position = vecA;
vertices[vertex++].position = vecB;
vertices[vertex++].position = vecC;
vertices[vertex++].position = vecD;
vertices[vertex++].position = vecE;
vertices[vertex++].position = vecF;
vertices[vertex++].position = vecG;
vertices[vertex++].position = vecH;
vertices[vertex++].position = vecI;
float halfStep = step * 0.5f;
int idxA = GetOrAddVertex( x, y, level );
int idxB = GetOrAddVertex( x + halfStep, y, level );
int idxC = GetOrAddVertex( x + step, y, level );
int idxD = GetOrAddVertex( x, y + halfStep, level );
int idxE = GetOrAddVertex( x + halfStep, y + halfStep, level );
int idxF = GetOrAddVertex( x + step, y + halfStep, level );
int idxG = GetOrAddVertex( x, y + step, level );
int idxH = GetOrAddVertex( x + halfStep, y + step, level );
int idxI = GetOrAddVertex( x + step, y + step, level );
// Stitch the border into the next level
if ( x == -radius )
{
// E G A
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 3;
indices[idx++] = vertex - 9;
indices.Add( idxE );
indices.Add( idxG );
indices.Add( idxA );
}
else
{
// E D A
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 6;
indices[idx++] = vertex - 9;
indices.Add( idxE );
indices.Add( idxD );
indices.Add( idxA );
// E G D
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 3;
indices[idx++] = vertex - 6;
indices.Add( idxE );
indices.Add( idxG );
indices.Add( idxD );
}
if ( y == radius - 1 )
if ( y == radius - step )
{
// E I G
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 1;
indices[idx++] = vertex - 3;
indices.Add( idxE );
indices.Add( idxI );
indices.Add( idxG );
}
else
{
// E H G
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 2;
indices[idx++] = vertex - 3;
indices.Add( idxE );
indices.Add( idxH );
indices.Add( idxG );
// E I H
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 1;
indices[idx++] = vertex - 2;
indices.Add( idxE );
indices.Add( idxI );
indices.Add( idxH );
}
if ( x == radius - 1 )
if ( x == radius - step )
{
// E C I
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 7;
indices[idx++] = vertex - 1;
indices.Add( idxE );
indices.Add( idxC );
indices.Add( idxI );
}
else
{
// E F I
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 4;
indices[idx++] = vertex - 1;
indices.Add( idxE );
indices.Add( idxF );
indices.Add( idxI );
// E C F
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 7;
indices[idx++] = vertex - 4;
indices.Add( idxE );
indices.Add( idxC );
indices.Add( idxF );
}
if ( y == -radius )
{
// E A C
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 9;
indices[idx++] = vertex - 7;
indices.Add( idxE );
indices.Add( idxA );
indices.Add( idxC );
}
else
{
// E B C
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 8;
indices[idx++] = vertex - 7;
indices.Add( idxE );
indices.Add( idxB );
indices.Add( idxC );
// E A B
indices[idx++] = vertex - 5;
indices[idx++] = vertex - 9;
indices[idx++] = vertex - 8;
indices.Add( idxE );
indices.Add( idxA );
indices.Add( idxB );
}
}
}
}
var mesh = new Mesh( material );
mesh.CreateVertexBuffer( vertex, PosAndLodVertex.Layout, vertices.AsSpan() );
mesh.CreateIndexBuffer( idx, indices.AsSpan() );
ArrayPool<PosAndLodVertex>.Shared.Return( vertices );
ArrayPool<int>.Shared.Return( indices );
mesh.CreateVertexBuffer( vertices.Count, PosAndLodVertex.Layout, vertices );
mesh.CreateIndexBuffer( indices.Count, indices );
return mesh;
}

View File

@@ -98,10 +98,12 @@ VS
if ( weight > 0.0f )
{
float2 layerUV = ( o.LocalPosition.xy / 32.0f ) * g_TerrainMaterials[matIdx].uvscale;
float2 seamlessUV = Terrain_SampleSeamlessUV( layerUV );
// Sample height from Height(blue channel) of NHO texture
Texture2D tNHO = Bindless::GetTexture2D( g_TerrainMaterials[matIdx].nho_texid );
float height = tNHO.SampleLevel( g_sAnisotropic, layerUV, 0 ).b;
float height = tNHO.SampleLevel( g_sAnisotropic, seamlessUV, 0 ).b;
float centeredHeight = (height - 0.5f) * 2.0f;
// Accumulate displacement
@@ -171,9 +173,13 @@ PS
for ( int i = 0; i < 4; i++ )
{
float2 layerUV = texUV * g_TerrainMaterials[ i ].uvscale;
float2 seamlessUV = Terrain_SampleSeamlessUV( layerUV );
float4 bcr = Bindless::GetTexture2D( g_TerrainMaterials[ i ].bcr_texid ).Sample( g_sAnisotropic, layerUV );
float4 nho = Bindless::GetTexture2D( g_TerrainMaterials[ i ].nho_texid ).Sample( g_sAnisotropic, layerUV );
Texture2D tBcr = Bindless::GetTexture2D( g_TerrainMaterials[ i ].bcr_texid );
Texture2D tNho = Bindless::GetTexture2D( g_TerrainMaterials[ i ].nho_texid );
float4 bcr = tBcr.Sample( g_sAnisotropic, seamlessUV );
float4 nho = tNho.Sample( g_sAnisotropic, seamlessUV );
float3 normal = ComputeNormalFromRGTexture( nho.rg );
normal.xz *= g_TerrainMaterials[ i ].normalstrength;

View File

@@ -71,6 +71,30 @@ class Terrain
}
};
// Get UV with per-tile UV offset to reduce visible tiling
// Works by offsetting UVs within each tile using a hash of the tile coordinate
float2 Terrain_SampleSeamlessUV( float2 uv )
{
float2 tileCoord = floor( uv );
float2 localUV = frac( uv );
// Generate random values for this tile
float2 hash = frac(tileCoord * float2(443.897f, 441.423f));
hash += dot(hash, hash.yx + 19.19f);
hash = frac((hash.xx + hash.yx) * hash.xy);
// Random rotation (0 to 2π)
float angle = hash.x * 6.28318530718;
float cosA = cos(angle);
float sinA = sin(angle);
float2x2 rot = float2x2(cosA, -sinA, sinA, cosA);
// Rotate around center
localUV = mul(rot, localUV - 0.5) + 0.5;
// Apply random offset
return tileCoord + frac(localUV + hash);
}
// Move to another file: