Decals is a class that handles whole process of rendering decals on scene. Usually you may need to use this class if you are building a custom shading model and you want it to support decal rendering. You don't have to use this function if your shader passes all data into a standard shading model, it already handles decals.
::Apply
void Decals::Apply( float3 WorldPosition, in out Material material )
WorldPositionis the world-space pixel positionMaterialis the material that you built in the main shader. Decals iterate over the existing material, they are applied on top of it.in outmeans that it will automatically write new changes to the material variable that you pass into this function.- Make sure that your
ScreenPositionin the material struct is not empty!
- Make sure that your
This is everything that you need to know about decals if you want to add them in your shader code. If you are curious about the internal stuff, you're welcome to continue reading.
Decals Culling
Counting Available Decals
All decals are culled using the clustered culling. To check how many decals are available in the current cluster, you do this:
ClusterRange range = Cluster::Query( ClusterItemType_Decal, ScreenPosition );
ClusterRange is a struct that holds a Count property, it will contain the number of decals present in cluster. Zero means that current cluster has no decals available. Do whatever you want with this. This range will be required to load the actual decal ID from cluster.
Reading from Cluster
Decals are stored in a structured buffer, but you need an ID to load correct decals from it. This ID is stored within the cluster data, this is how you fetch it:
// Iterate through all decals in cluster, load and resolve them
for ( uint j = 0; j < range.Count; j++ )
{
Decal decal = DecalBuffer[ Cluster::LoadItem( range, j ) ];
// Do whatever...
}
Decal Data Structure
All decals are stored in a global structured buffer called DecalBuffer (bound to the DecalsBuffer attribute). Each decal instance stores following data:
struct Decal
{
float3 WorldPosition; // Decal position in world
float4 Quat; // Decal rotation
float3 Scale; // Decal scale
uint2 PackedTextureID; // Packed bindless texture IDs, attenuation, exponent
uint SortOrder; // unused
uint ExclusionBitMask; // unused
uint ColorTint; // Decal tint
int ExtraDataOffset; // Byte address offset for extra data struct
};
PackedTextureID
PackedTextureID is a packed uint2 vector that stores bindless IDs for standard decal textures and additional values, such as decal attenuation and color tint exponent.
Standard decal textures are: color, normal and RMA (packed roughness/metalness/AO). Red channel is roughness, green channel is metalness, and blue is ambient occlusion. For RMA, it is expected that artist must manually pack these textures together using that specific order. Additional texture slots such as heightmap/emission are stored in the extra data buffer.
Here is an example how to unpack this data:
// Unpacking bindless texture IDs
uint nColorId = decal.PackedTextureID.x & 0xFFFF;
uint nNormalId = ( decal.PackedTextureID.x >> 16 ) & 0xFFFF;
uint nRmaId = decal.PackedTextureID.y & 0xFFFF;
// Unpacking decal attenuation and exponent
uint nDecalAttenuation = ( decal.PackedTextureID.y >> 24 ) & 0xFF;
float flAttenuation = (float)nDecalAttenuation / 255.0f; // Attenuation must be a float
uint nColorExponent = ( decal.PackedTextureID.y >> 16 ) & 0xFF;
Color Tint
Color tint property in this struct is also packed. To get the actual color value, Decal class file has a helper function UnpackColor( uint packedColor ). Use it to get the actual float4 color tint value.
Here's an example how to unpack the tint color, get the signed value, and then apply color tint exponent:
// Unpack the tint color and get a signed vector
float4 vColorTint = UnpackColor( decal.ColorTint );
float3 vSignedRGB = vColorTint.rgb * 255.0 - 128.0;
// Apply exponent to the color tint
vColorTint.rgb = vSignedRGB * exp2( float( nColorExponent ) - 128.0 - 7.0 );
Helper Struct Functions
There are two helper functions included with the main buffer:
GetCenter()returns-WorldPositionGetRadius()returns the radius of this decal in world units
None of these functions are used in the actual decal resolving code.
Extra Decal Data
To avoid cluttering the main struct with every single feature, there is a dedicated byte address buffer called DecalsExtraDataBuffer which stores all extra data for optional features. An integer property ExtraDataOffset from main buffer tells you where exactly the data for current decal is stored.
At the current moment, extra data struct looks like this:
| Type | Property Description | Byte Offset |
|---|---|---|
| float4 | Sheet Data | 0 |
| uint | Sequence Index | 16 |
| float | Color Mix Amount | 20 |
| uint | Feature flags (unused) | 24 |
| uint | Heightmap Bindless ID | 28 |
| float | Parallax Strength | 32 |
| uint | SamplerState Bindless ID | 36 |
| uint | Emission Bindless ID | 40 |
| float | Emission Strength | 44 |
| uint | Height Coverage Settings (packed) | 48 |
Not every decal has its own extra data allocated in this buffer. By default, no extra data is allocated at all if decal does not match at least one of these conditions:
- Heightmap texture is added
- Emission texture is added
- Color map texture has a sheet sequence
- Color mix value is less than
1.0f - Using non-default sampler (texture filter mode)
Here's an example how to load some of this data:
// Safety check, avoid reading extra data directly since some decals
// may not have any extra data allocated at all!
if ( decal.ExtraDataOffset >= 0 )
{
float4 vSheetData = asfloat( DecalsExtraDataBuffer.Load4( decal.ExtraDataOffset ) );
uint nSheetSequence = DecalsExtraDataBuffer.Load( decal.ExtraDataOffset + 16 );
float flColorMix = asfloat( DecalsExtraDataBuffer.Load( decal.ExtraDataOffset + 20 ) );
uint nHeightBindlessID = DecalsExtraDataBuffer.Load( decal.ExtraDataOffset + 28 );
uint nSamplerBindlessID = DecalsExtraDataBuffer.Load( decal.ExtraDataOffset + 36 );
uint nEmissionBindlessID = DecalsExtraDataBuffer.Load( decal.ExtraDataOffset + 40 );
}
Height Coverage
Height Coverage settings are encoded in a single uint. Use the example below to load settings, and extract floats for coverage amount/range.
// Height coverage is available ONLY if decal has a valid height texture!
// So make sure to get these values after checking for valid heightmap first.
if ( nHeightBindlessID > 0 )
{
// Load height coverage settings from extra buffer
uint nHeightCoverageSettings = DecalsExtraDataBuffer.Load( decal.ExtraDataOffset + 48 );
// Decode settings and get coverage amount & range from them
float flCoverageAmount = 1.0f - (( nHeightCoverageSettings & 0xFF ) / 255.0);
float flCoverageRange = ( ( nHeightCoverageSettings >> 8 ) & 0xFF ) / 255.0;
}
Also keep in mind that both coverage settings are hardcode clamped:
- Coverage Amount is clamped within
0 to 1range - Coverage Range is clamped within
0 to 0.5range
Standard math for calculating coverage & range looks like this:
float flCoverageRangeClamped = min( flCoverageRange, min( flCoverageAmount, 1.0 - flCoverageAmount ) );
float flHeightCoverageMask = smoothstep( flCoverageAmount - flCoverageRangeClamped, flCoverageAmount + 0.001, flHeightmap );
Helper Functions
Transform Point
float3 TransformPoint( Decal decal, float3 position )
- Applies world transforms of this decal.
positionis a world-space pixel position, passed from your main shader/material.
Decal Attenuation
Calculates the attenuation masking of this decal based on decal orientation and surface normals. It gives a nice and cheap effect of projection.
float DecalAttenuation( float3 SurfaceNormal, float3 DecalOrientation, float AttenuationFactor )
SurfaceNormalis expected to be world-space normals, for example normals from Material structDecalOrientationis the direction of this decalAttenuationFactoris the amount of attenuation within 0-1 range. This is the value that you unpack from uint2PackedTextureIDproperty

Quaternion To Matrix
Converts float4 quaternion vector value to float3x3 matrix.
float3x3 QuaternionToMatrix( float4 q )


