s&box comes with a terrain system and fairly large shader backend for various terrain things: meshlets, encoded control map, compact material format, structs, alpha clipping, and some other features.
🔨👶 Terrain system is still receiving new updates and it's probably fair to warn that this API may change over the time. If that happens, we will make sure to reflect these changes on this page.
Terrain Shader
Terrain shader lives in /core/shaders/terrain.shader. This is a main shader that is used on terrain by default. You can swap the material to something else (like material with a custom terrain shader) in the component settings.
It uses its own unique vertex & pixel input structs. Vertex input holds only vertex position, LOD, and instance ID. All other mesh data like geometry normals, tangents and UVs are all sampled/calculated within a pixel shader. Pixel input holds local/world position of a terrain vertex, as well as screen position (pixel shader only) and pixel position (vertex shader only).
Terrain Class
All of this lives in /core/shaders/terrain/TerrainCommon.hlsl, nothing of this will work if you don't have this file included in the shader.
Receiving Terrain Struct
TerrainStruct Terrain::Get()-
- Returns the struct data for terrain
Terrain Geometry Maps
Texture2D Terrain::GetHeightMap()-
- Returns a Texture2D object of terrain's heightmap.
Texture2D Terrain::GetControlMap()-
- Returns a Texture2D object of terrain's indexed control map.
Texture2D Terrain::GetNormalMap()-
- Returns a Texture2D object of terrain's baked geometry normals. Don't forget to remap them to [-1 to 1] range after sampling!
Terrain Geometry Normals
float3 Terrain::SampleNormal( float2 uv )-
- Returns remapped [-1 to 1] terrain's baked geometry normals as
float3
- Returns remapped [-1 to 1] terrain's baked geometry normals as
-
uvmust be local terrain UV coordinates
float3 Terrain::NormalBasis( float2 uv, out float3 tangentU, out float3 tangentV )-
- Returns remapped [-1 to 1] terrain's baked geometry normals as
float3AND also provides tangentU & tangentV
- Returns remapped [-1 to 1] terrain's baked geometry normals as
-
- Same as above,
uvmust be local terrain UV coordinates
- Same as above,
Coordinates
float3 Terrain::WorldToLocal( float3 worldPos )-
- Converts provided world-space position to local terrain coordinates.
float2 Terrain::LocalToUV( float2 localPos )-
- Converts local terrain coordinates to proper UV coords in [0 to 1] range that can be used for sampling terrain textures
float2 Terrain::GetUV( float3 worldPos )-
- Converts provided world-space position straight to terrain's UV coordinates.
Terrain Heightmap
float Terrain::GetHeight( float2 localPos )-
- Returns terrain height at given local terrain coordinates, with height scale applied. Keep in mind that returned height value is local to terrain, it doesn't reflect its actual position in world.
float Terrain::GetWorldHeight( float3 worldPos )-
- Returns terrain height at given world-space position, with height scale applied. This height will be in world-space coordinates, representing the Z component, not local to terrain.
float Terrain::GetDistanceToSurface( float3 worldPos )-
- Returns distance to terrain surface at given world-space position.
Misc
int Terrain::Count()-
- Returns all terrains on scene.
bool Terrain::IsInBounds( float3 worldPos )-
- Checks if provided world-space position is within terrain bounds.
float Terrain::GetBlendFactor( float3 worldPos, float blendLength )-
- Get a 0-1 blend factor for mesh blending based on distance to terrain surface.
-
- Returns 1 at terrain surface, fading to 0 at blendLength distance above
Texture Sampling
See section "Sampling Terrain from Any Shader" below if you'd like to sample a full terrain material instead of color.
float3 Terrain::SampleMaterialColor( float2 texUV, CompactTerrainMaterial material )-
- Returns given material's color map, must provide terrain UV coordinates.
-
- Must provide an exact terrain material to sample color from it
-
- You can also add a third argument
int mipLevelto this function to get specific mip level of this material's color texture.
- You can also add a third argument
float3 Terrain::SampleColor( float3 worldPos )-
- Samples color map of terrain from given world-space coordinates, with material blending and other necessary steps. Keep in mind that this is only color, not full terrain material.
-
- Add a second argument
int mipLevelto get exact mip level
- Add a second argument
Sampling Terrain From Any Shader
We have a common API for sampling a complete terrain splat with all necessary steps applied: calculating weights and gathering control bits, decoding control map, fetching terrain materials and blending all materials at given pixel together. It is used by main terrain shader as well, so if you use this function from your own custom shader, it will have visuals fully identical to the terrain mesh.
Returned data is represented as a Material struct, it holds all terrain textures except for heightmap, since we don't have a place for it in the material struct sadly.
Material Terrain::Sample( float3 worldPos, bool bUseGeometricNormals = false )-
- Samples terrain splat at given world-space coordinates.
-
- Please note that by default this function will return plain terrain splat without transforming them with terrain's geometry normals. Set the second argument to
trueif you want to sample them as well.
- Please note that by default this function will return plain terrain splat without transforming them with terrain's geometry normals. Set the second argument to
Material Terrain::Sample( float2 localPos, bool bUseGeometricNormals = false )-
- Does the same thing as function above, but this one accepts local terrain coordinates, in case you have them in your code available already.
Material Terrain::Sample( float2 localPos, float2 localDdx, float2 localDdy, uint4 controlBits, float4 quadWeights, bool bUseGeometricNormals = false )-
- Samples terrain splat at given local terrain coordinates, but here you can pass your own control bits and weights.
You can also transform tangent normals with terrain's baked normals separately:
void Terrain::ApplyGeometricNormals( inout Material m, float2 uv )-
- Blends tangent normals in provided material with terrain's baked geometry normals. Returns nothing, it will iterate on given material struct.
-
- Expects terrain UV coordinates. If you don't have them, you can use
Terrain::GetUV.
- Expects terrain UV coordinates. If you don't have them, you can use
-
- Under the hood it samples terrain's baked normal map and then calculates tangentU/tangentV, re-orthonormalizes to account for possible custom terrain scale, and then finally blends normals. It will also store tangentU and tangentV into Material's
WorldTangentUandWorldTangentVaccordingly, in case you will need them for anything else later on.
- Under the hood it samples terrain's baked normal map and then calculates tangentU/tangentV, re-orthonormalizes to account for possible custom terrain scale, and then finally blends normals. It will also store tangentU and tangentV into Material's
Using on models
This API can be used in any shader, not just for custom terrain shaders, it will work with any models as well. To use it in a regular shader, you need to include terrain/TerrainCommon.hlsl in pixel shader (PS section), and then you're good to go. Here's a basic example:
float4 MainPs( PixelInput i ) : SV_Target0
{
Material basicMaterial = Material::Init( i.vPositionWithOffsetWs, i.vPositionSs );
// ... setup your basic material here...
// and then sample terrain at given world-space position, with geometric normals applied
Material terrain = Terrain::Sample( m.WorldPosition, true );
// blend two materials together, very shoddy example but you get the idea
Material final = Material::lerp( basicMaterial, terrain, 0.5f );
return ShadingModelStandard::Shade( i, final );
}
Terrain Struct
Each terrain instance holds following data in its struct:
struct TerrainStruct
{
float4x4 Transform;
float4x4 TransformInv;
int HeightMapTexture;
int ControlMapTexture;
float UnitsPerTexel;
float HeightScale;
bool HeightBlending;
float HeightBlendSharpness;
int samplerindex;
int NormalMapTexture;
};
TransformandTransformInvare terrain's world transform (and inverse transform) matrix, you need to use it to get proper local terrain coordinates that match its transform on scene. Transform holds terrain's position, rotation and scale.HeightMapTexture,ControlMapTextureandNormalMapTextureare bindless IDs for terrain's heightmap, control map and baked geometry normals.UnitsPerTexelcontrols the density of texture tiling.HeightScaleis a setting that controls the total height of terrain in units. So if height scale is1000then it means that terrain's physical maximum height will be 1000 unitsHeightBlendingis a bool that controls whether this terrain uses height blending for splats. If it's disabled, then simpler bilinear blending will be used instead.HeightBlendSharpnesscontrols the sharpness of height blending between splatssamplerindexis a bindless ID for sampler state, which will be used for sampling terrain textures
To avoid referring to a full buffer name every time you need to get something from terrain data, you can use Terrain::Get() instead. So if you want to grab terrain's height scale, you just do Terrain::Get().HeightScale, or Terrain::Get().Transform to grab terrain's world transform matrix.
Terrain Material
Each terrain material holds a bunch of data in its struct:
struct TerrainMaterial
{
int bcr_texid;
int nho_texid;
float uvscale;
uint flags;
float metalness;
float heightstrength;
float normalstrength;
float displacementscale;
bool HasFlag( TerrainFlags flag )
{
return (flags & flag) != 0;
}
};
bcr_texidis a bindless ID of material's color map texture (stored in RGB) and roughness (alpha). You need to applySrgbGammaToLinearon RGB color map to get accurate visuals.nho_texidis a bindless ID of material's tangent normal map (RG channels), heightmap (blue channel) and ambient occlusion (alpha channel). Keep in mind that this texture provides only normal map's RG maps, blue channel is later reconstructed usingComputeNormalFromRGTexture()helper.uvscaleis material's UV scaleflagsis used for storing various flags in provided material. Currently there's only just one terrain flag, which isNoTilemetalnessis a float value in 0-1 range indicating how metallic is this material. It isn't a texture, nor a bindless ID for it, just uniform float value.heightstrengthis a float value for controlling the strength of material's heightmap texture.normalstrengthis a float value for controlling the intensity of material's normal map.displacementscaleis a float value for controlling the intensity of terrain displacement.HasFlag( flag )checks if current material has the provided flag. As it's stated above, currently there's onlyNoTileflag.
All terrain materials are stored in a structured buffer named g_TerrainMaterials. To get an exact terrain material you must know its index, one of the ways to do so is to decode control map and get base/overlay material ID from CompactTerrainMaterial struct. For more details, see the next section.
Encoded Terrain Control Map
Our terrain system uses encoded control map which supports up to 32 terrain maps. When texel is decoded, it provides a CompactTerrainMaterial struct. Its structure looks like this:
struct CompactTerrainMaterial
{
uint BaseTextureId;
uint OverlayTextureId;
uint BlendFactor;
bool IsHole;
uint Reserved;
}
BaseTexureIdpoints at base material's ID in global terrain materials buffer (g_TerrainMaterials)OverlayTextureIdpoints at overlay material's ID in global terrain materials bufferBlendFactoris a blend factor between base and overlay materials, where 0 = full base, and 255 = full overlay. This value must be remapped to [0 to 1] before being used in actual blending math. See below for details.IsHoleis a flag that indicates that given texel is a holeReservedis reserved (duh) and contains nothing at the moment
So basically every encoded control map texel holds two terrain materials inside of it, along with information for alpha holes and blend factor for them, it doesn't store actual terrain material textures. So for example, to get an actual struct that holds texture IDs for base texture, you'd need to do this:
// where 'material' is CompactTerrainMaterial
TerrainMaterial baseMat = g_TerrainMaterials[material.BaseTextureId];
Decoding
Structure of encoded control map pixel looks like this:
| Bits | Purpose | Limit |
|---|---|---|
| 0-4 | Base texture ID | 0-31 |
| 5-9 | Overlay texture ID | 0-31 |
| 10-17 | Blend factor between base & overlay | 0-255 |
| 18 | Hole flag (1 = hole, 0 = solid) | ... |
| 19-31 | Reserved, 13 bits |
There are two ways to decode this struct and get useable data:
- Get control quads (
Terrain::GatherControlQuad) to fetch 4 texels of bilinear quad around the UV as rawuintbits from terrain's control map, and then decode each bit usingCompactTerrainMaterial::Decode( uint packed )
CompactTerrainMaterial mat00 = CompactTerrainMaterial::Decode( controlBits.x );
CompactTerrainMaterial mat10 = CompactTerrainMaterial::Decode( controlBits.y );
CompactTerrainMaterial mat01 = CompactTerrainMaterial::Decode( controlBits.z );
CompactTerrainMaterial mat11 = CompactTerrainMaterial::Decode( controlBits.w );
- Alternatively, you can decode from control map stored in terrain's GPU storage using
CompactTerrainMaterial::DecodeFromFloat( float control )without four control quads, which is what commonly used by compute shaders.
Encoding
This has no use in live terrain sampling, but may be useful if you are interacting with terrain control map from a compute shader and want to edit it.
Once you finish editing values inside this struct, use Encode() to get encoded uint control map. You can also use EncodeToFloat() to encode it as a float instead of uint, since control map texture is stored as a float in the terrain's GPU storage.
For example, in cs_terrain_splat compute shader, which is used for editing terrain splat using GPU, once we're done modifying what we need, we write back to this control map at given texel like this:
// 'ControlMap' is a RWTexture2D control map which we edit in this shader
// 'material' is a CompactTerrainMaterial struct
ControlMap[texel] = material.EncodeToFloat();
Blend
Every terrain material stores blend value, but even after decoding it is represented in [0 to 255] range. Before applying blending between base and overlay materials, make sure to use GetNormalizedBlend() which will remap the value to [0 to 1] range.
CompactTerrainMaterial mat = /* <...> */;
float materialBlend = mat.GetNormalizedBlend(); // returns material blend in 0-1 range
When editing control map from compute shaders, make sure to remap this value back to [0 to 255] range before writing encoded control map texel back to RW texture:
// remapping 0-1 blend back to 0-255 range before writing control map to disk
material.BlendFactor = uint( saturate( overlayWeight ) * 255.0 + 0.5 );