s&box
RustCounter-Strike 2s&boxDeadlock
Wiki
Get s&box on Steam
Browse
OverviewGetting StartedCreating GamesEngine & ToolsDocumentationCommunity GamesResources
s&boxDocumentationRenderingShadersClassesDecals
SOURCE 2 · C#
Build and play games with s&box.

Facepunch's game engine and creation platform — make games in C#, or jump straight into community games.

Get s&box on Steam

s&box Wiki

  • Overview
  • Getting Started
  • Creating Games
  • Engine & Tools
  • Documentation
  • Community Games
  • Resources

Tools & Community

  • Getting Started
  • Creating Games
  • Engine & Tools
  • Resources

Platforms

  • RustBattle
  • CSBattle
  • Rust Game Wiki
  • CS2 Skins Wiki
  • Deadlock Wiki

About

  • Official Site
  • Developer Wiki
  • GitHub
  • Get on Steam
NOT AFFILIATED WITH FACEPUNCH STUDIOS · © 2026 s&box Wiki (community)
Docs/Rendering
Rendering

Decals

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 )
  • WorldPosition is the world-space pixel position
  • Material is the material that you built in the main shader. Decals iterate over the existing material, they are applied on top of it. in out means that it will automatically write new changes to the material variable that you pass into this function.
    • Make sure that your ScreenPosition in the material struct is not empty!

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 -WorldPosition
  • GetRadius() 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:

TypeProperty DescriptionByte Offset
float4Sheet Data0
uintSequence Index16
floatColor Mix Amount20
uintFeature flags (unused)24
uintHeightmap Bindless ID28
floatParallax Strength32
uintSamplerState Bindless ID36
uintEmission Bindless ID40
floatEmission Strength44
uintHeight 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 1 range
  • Coverage Range is clamped within 0 to 0.5 range

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. position is 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 )

  • SurfaceNormal is expected to be world-space normals, for example normals from Material struct
  • DecalOrientation is the direction of this decal
  • AttenuationFactor is the amount of attenuation within 0-1 range. This is the value that you unpack from uint2 PackedTextureID property

Quaternion To Matrix

Converts float4 quaternion vector value to float3x3 matrix.

float3x3 QuaternionToMatrix( float4 q )

Source: Facepunch/sbox-docs (CC-BY-4.0) · updated 2026-06-22. Read it rendered on the official docs.

More in Rendering

AmbientLight
Animated Effects
Attributes and Variables
BeamEffect
Beams
Bindless API
C# Shader Nodes
Classes
← All documentation

Community wiki — not affiliated with Facepunch Studios. For the latest and most authoritative information, see the official developer wiki.