Add terrain flags and make NoTile an option (#3575)

* Add terrain flags and make NoTile an option
This makes removes unused uvrotation from material
Fixes NRE if storage is null
Adds flags enum per material for future use
https://files.facepunch.com/sampavlovic/1b0911b1/ShareX_fKdgnanxbh.mp4

* Default NoTiling to false

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* The changes from commit e0c1c801c0 to fix normals on notile got accidentally reverted on 87a6fde918, readd normals rotation for notile, update shaders

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Sam Pavlovic
2025-12-10 07:19:03 -03:00
committed by GitHub
parent dbca8f8fa0
commit c227181ca2
4 changed files with 122 additions and 41 deletions

View File

@@ -1,8 +1,15 @@
using System.IO;
using System.IO;
using System.Text.Json.Serialization;
namespace Sandbox;
[Flags]
public enum TerrainFlags : uint
{
None = 0,
NoTile = 1 << 0
}
/// <summary>
/// Description of a Terrain Material.
/// </summary>
@@ -25,7 +32,6 @@ public class TerrainMaterial : GameResource
[JsonIgnore, Hide] public Texture NHOTexture { get; private set; }
[Category( "Material" ), Title( "UV Scale" )] public float UVScale { get; set; } = 1.0f;
[Category( "Material" ), Title( "UV Rotation" )] public float UVRotation { get; set; } = 0.0f;
[Category( "Material" ), Range( 0.0f, 1.0f )] public float Metalness { get; set; } = 0.0f;
[Category( "Material" ), Range( 0.1f, 10 )] public float NormalStrength { get; set; } = 1.0f;
[Category( "Material" ), Range( 0.1f, 10 )] public float HeightBlendStrength { get; set; } = 1.0f;
@@ -36,6 +42,23 @@ public class TerrainMaterial : GameResource
[Category( "Material" ), Range( 0.0f, 10.0f ), Title( "Displacement Scale" ), ShowIf( nameof( HasHeightTexture ), true )]
public float DisplacementScale { get; set; } = 0.0f;
[Category( "Material" ), Title( "No Tiling" )]
public bool NoTiling { get; set; } = false;
[JsonIgnore, Hide]
public TerrainFlags Flags
{
get
{
var flags = TerrainFlags.None;
if ( NoTiling )
flags |= TerrainFlags.NoTile;
return flags;
}
}
[Category( "Misc" )] public Surface Surface { get; set; }
void LoadGeneratedTextures()

View File

@@ -46,7 +46,7 @@ public partial class Terrain
_so.Flags.CastShadows = RenderType == ShadowRenderType.On || RenderType == ShadowRenderType.ShadowsOnly;
// If we have no textures, push a grid texture (SUCKS)
_so.Attributes.SetCombo( "D_GRID", Storage.Materials.Count == 0 );
_so.Attributes.SetCombo( "D_GRID", Storage?.Materials.Count == 0 );
_clipMapLodLevels = ClipMapLodLevels;
_clipMapLodExtentTexels = ClipMapLodExtentTexels;
@@ -70,13 +70,13 @@ public partial class Terrain
public float HeightBlendSharpness;
}
[StructLayout( LayoutKind.Sequential, Pack = 0 )]
[StructLayout( LayoutKind.Sequential )]
private struct GPUTerrainMaterial
{
public int BCRTextureID;
public int NHOTextureID;
public float UVScale;
public float UVRotation;
public TerrainFlags Flags;
public float Metalness;
public float HeightBlendStrength;
public float NormalStrength;
@@ -153,11 +153,11 @@ public partial class Terrain
BCRTextureID = layer?.BCRTexture?.Index ?? 0,
NHOTextureID = layer?.NHOTexture?.Index ?? 0,
UVScale = 1.0f / (layer?.UVScale ?? 1.0f),
UVRotation = layer?.UVRotation ?? 1.0f,
Metalness = layer?.Metalness ?? 0.0f,
NormalStrength = 1.0f / (layer?.NormalStrength ?? 1.0f),
HeightBlendStrength = layer?.HeightBlendStrength ?? 1.0f,
DisplacementScale = layer?.DisplacementScale ?? 0.0f,
Flags = layer?.Flags ?? TerrainFlags.None,
};
}
@@ -167,6 +167,6 @@ public partial class Terrain
Scene.RenderAttributes.Set( "TerrainMaterials", MaterialsBuffer );
// If we have no textures, push a grid texture (SUCKS)
_so.Attributes.SetCombo( "D_GRID", Storage.Materials.Count == 0 );
_so.Attributes.SetCombo( "D_GRID", Storage?.Materials.Count == 0 );
}
}

View File

@@ -97,14 +97,24 @@ VS
CompactTerrainMaterial material = CompactTerrainMaterial::DecodeFromFloat( rawPixel );
// Sample base material displacement
float2 baseLayerUV = ( o.LocalPosition.xy / 32.0f ) * g_TerrainMaterials[material.BaseTextureId].uvscale;
float4 baseNho = Bindless::GetTexture2D( g_TerrainMaterials[material.BaseTextureId].nho_texid ).SampleLevel( g_sAnisotropic, baseLayerUV, 0 );
float baseDisplacement = baseNho.b * g_TerrainMaterials[material.BaseTextureId].displacementscale;
TerrainMaterial mat = g_TerrainMaterials[material.BaseTextureId];
float2 baseLayerUV = ( o.LocalPosition.xy / 32.0f ) * mat.uvscale;
if( mat.HasFlag( TerrainFlags::NoTile ) )
baseLayerUV = Terrain_SampleSeamlessUV( baseLayerUV );
float4 baseNho = Bindless::GetTexture2D( mat.nho_texid ).SampleLevel( g_sAnisotropic, baseLayerUV, 0 );
float baseDisplacement = baseNho.b * mat.displacementscale;
// Sample overlay material displacement
float2 overlayLayerUV = ( o.LocalPosition.xy / 32.0f ) * g_TerrainMaterials[material.OverlayTextureId].uvscale;
float4 overlayNho = Bindless::GetTexture2D( g_TerrainMaterials[material.OverlayTextureId].nho_texid ).SampleLevel( g_sAnisotropic, overlayLayerUV, 0 );
float overlayDisplacement = overlayNho.b * g_TerrainMaterials[material.OverlayTextureId].displacementscale;
mat = g_TerrainMaterials[material.OverlayTextureId];
float2 overlayLayerUV = ( o.LocalPosition.xy / 32.0f ) * mat.uvscale;
if( mat.HasFlag( TerrainFlags::NoTile ) )
overlayLayerUV = Terrain_SampleSeamlessUV( overlayLayerUV );
float4 overlayNho = Bindless::GetTexture2D( mat.nho_texid ).SampleLevel( g_sAnisotropic, overlayLayerUV, 0 );
float overlayDisplacement = overlayNho.b * mat.displacementscale;
// Blend between base and overlay displacement
float blend = material.GetNormalizedBlend();
@@ -265,21 +275,33 @@ PS
// Sample materials by index
for ( int i = 0; i < 4; i++ )
{
float2 layerUV = texUV * g_TerrainMaterials[ indices[i] ].uvscale;
TerrainMaterial mat = g_TerrainMaterials[ i ];
float2 layerUV = texUV * mat.uvscale;
float2x2 uvAngle = float2x2( 1, 0, 0, 1 );
float4 bcr = Bindless::GetTexture2D( g_TerrainMaterials[ indices[i] ].bcr_texid ).Sample( g_sAnisotropic, layerUV );
float4 nho = Bindless::GetTexture2D( g_TerrainMaterials[ indices[i] ].nho_texid ).Sample( g_sAnisotropic, layerUV );
// Apply NoTile if needed
if ( mat.HasFlag( TerrainFlags::NoTile ) )
{
layerUV = Terrain_SampleSeamlessUV( layerUV, uvAngle );
}
float3 n = ComputeNormalFromRGTexture( nho.rg );
n.xz *= g_TerrainMaterials[ indices[i] ].normalstrength;
n = normalize( n );
Texture2D tBcr = Bindless::GetTexture2D( mat.bcr_texid );
Texture2D tNho = Bindless::GetTexture2D( mat.nho_texid );
float4 bcr = tBcr.Sample( g_sAnisotropic, layerUV );
float4 nho = tNho.Sample( g_sAnisotropic, layerUV );
float3 normal = ComputeNormalFromRGTexture( nho.rg );
normal.xy = mul( uvAngle, normal.xy );
normal.xz *= mat.normalstrength;
normal = normalize( normal );
albedos[i] = SrgbGammaToLinear( bcr.rgb );
normals[i] = n;
normals[i] = normal;
roughnesses[i] = bcr.a;
heights[i] = nho.b * g_TerrainMaterials[ indices[i] ].heightstrength;
heights[i] = nho.b * mat.heightstrength;
aos[i] = nho.a;
metalness[i] = g_TerrainMaterials[ indices[i] ].metalness;
metalness[i] = mat.metalness;
}
// Normalize base weights
@@ -354,26 +376,42 @@ PS
{
texUV /= 32;
// Sample base material using seamless UV
float2 baseUV = texUV * g_TerrainMaterials[material.BaseTextureId].uvscale;
float2 baseSeamlessUV = Terrain_SampleSeamlessUV( baseUV );
// Sample base material with optional seamless UVs when requested
TerrainMaterial baseMat = g_TerrainMaterials[material.BaseTextureId];
float2 baseUV = texUV * baseMat.uvscale;
float2x2 baseUvAngle = float2x2( 1, 0, 0, 1 );
float2 baseSampleUV = baseUV;
if ( baseMat.HasFlag( TerrainFlags::NoTile ) )
{
baseSampleUV = Terrain_SampleSeamlessUV( baseUV, baseUvAngle );
}
float4 baseBcr = Bindless::GetTexture2D( g_TerrainMaterials[material.BaseTextureId].bcr_texid ).Sample( g_sAnisotropic, baseSeamlessUV );
float4 baseNho = Bindless::GetTexture2D( g_TerrainMaterials[material.BaseTextureId].nho_texid ).Sample( g_sAnisotropic, baseSeamlessUV );
float4 baseBcr = Bindless::GetTexture2D( baseMat.bcr_texid ).Sample( g_sAnisotropic, baseSampleUV );
float4 baseNho = Bindless::GetTexture2D( baseMat.nho_texid ).Sample( g_sAnisotropic, baseSampleUV );
float3 baseNormal = ComputeNormalFromRGTexture( baseNho.rg );
baseNormal.xz *= g_TerrainMaterials[material.BaseTextureId].normalstrength;
baseNormal.xy = mul( baseUvAngle, baseNormal.xy );
baseNormal.xz *= baseMat.normalstrength;
baseNormal = normalize( baseNormal );
// Sample overlay material using seamless UV
float2 overlayUV = texUV * g_TerrainMaterials[material.OverlayTextureId].uvscale;
float2 overlaySeamlessUV = Terrain_SampleSeamlessUV( overlayUV );
// Sample overlay material with optional seamless UVs when requested
TerrainMaterial overlayMat = g_TerrainMaterials[material.OverlayTextureId];
float2 overlayUV = texUV * overlayMat.uvscale;
float2x2 overlayUvAngle = float2x2( 1, 0, 0, 1 );
float2 overlaySampleUV = overlayUV;
if ( overlayMat.HasFlag( TerrainFlags::NoTile ) )
{
overlaySampleUV = Terrain_SampleSeamlessUV( overlayUV, overlayUvAngle );
}
float4 overlayBcr = Bindless::GetTexture2D( g_TerrainMaterials[material.OverlayTextureId].bcr_texid ).Sample( g_sAnisotropic, overlaySeamlessUV );
float4 overlayNho = Bindless::GetTexture2D( g_TerrainMaterials[material.OverlayTextureId].nho_texid ).Sample( g_sAnisotropic, overlaySeamlessUV );
float4 overlayBcr = Bindless::GetTexture2D( overlayMat.bcr_texid ).Sample( g_sAnisotropic, overlaySampleUV );
float4 overlayNho = Bindless::GetTexture2D( overlayMat.nho_texid ).Sample( g_sAnisotropic, overlaySampleUV );
float3 overlayNormal = ComputeNormalFromRGTexture( overlayNho.rg );
overlayNormal.xz *= g_TerrainMaterials[material.OverlayTextureId].normalstrength;
overlayNormal.xy = mul( overlayUvAngle, overlayNormal.xy );
overlayNormal.xz *= overlayMat.normalstrength;
overlayNormal = normalize( overlayNormal );
// Get normalized blend factor
@@ -382,8 +420,8 @@ PS
// Height blending if enabled
if ( Terrain::Get().HeightBlending )
{
float baseHeight = baseNho.b * g_TerrainMaterials[material.BaseTextureId].heightstrength;
float overlayHeight = overlayNho.b * g_TerrainMaterials[material.OverlayTextureId].heightstrength;
float baseHeight = baseNho.b * baseMat.heightstrength;
float overlayHeight = overlayNho.b * overlayMat.heightstrength;
float heightDiff = overlayHeight - baseHeight;
float sharpness = Terrain::Get().HeightBlendSharpness * 10.0;
@@ -395,7 +433,7 @@ PS
normal = lerp( baseNormal, overlayNormal, blend );
roughness = lerp( baseBcr.a, overlayBcr.a, blend );
ao = lerp( baseNho.a, overlayNho.a, blend );
metal = lerp( g_TerrainMaterials[material.BaseTextureId].metalness, g_TerrainMaterials[material.OverlayTextureId].metalness, blend );
metal = lerp( baseMat.metalness, overlayMat.metalness, blend );
}
//
@@ -528,4 +566,4 @@ PS
return ShadingModelStandard::Shade( p );
}
}
}

View File

@@ -5,6 +5,7 @@
// Not stable, shit will change and custom shaders using this API will break until I'm satisfied.
// But they will break for good reason and I will tell you why and how to update.
//
// 12/9/25: Added NoTile Flag
// 23/07/24: Initial global structured buffers
//
@@ -31,16 +32,26 @@ struct TerrainStruct
float HeightBlendSharpness;
};
enum TerrainFlags
{
NoTile = 1 // (1 << 0)
};
struct TerrainMaterial
{
int bcr_texid;
int nho_texid;
float uvscale;
float uvrotation;
uint flags;
float metalness;
float heightstrength;
float normalstrength;
float displacementscale;
bool HasFlag( TerrainFlags flag )
{
return (flags & flag) != 0;
}
};
SamplerState g_sBilinearBorder < Filter( BILINEAR ); AddressU( BORDER ); AddressV( BORDER ); >;
@@ -71,7 +82,7 @@ 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 Terrain_SampleSeamlessUV( float2 uv, out float2x2 uvAngle )
{
float2 tileCoord = floor( uv );
float2 localUV = frac( uv );
@@ -87,6 +98,9 @@ float2 Terrain_SampleSeamlessUV( float2 uv )
float sinA = sin(angle);
float2x2 rot = float2x2(cosA, -sinA, sinA, cosA);
// Output rotation matrix
uvAngle = rot;
// Rotate around center
localUV = mul(rot, localUV - 0.5) + 0.5;
@@ -94,6 +108,12 @@ float2 Terrain_SampleSeamlessUV( float2 uv )
return tileCoord + frac(localUV + hash);
}
float2 Terrain_SampleSeamlessUV( float2 uv )
{
float2x2 dummy;
return Terrain_SampleSeamlessUV( uv, dummy );
}
//
// Takes 4 samples
// This is easy for now, an optimization would be to generate this once in a compute shader
@@ -165,4 +185,4 @@ float4 Terrain_WireframeColor( uint lodLevel )
}
#endif
#endif
#endif