Skip to main content
s&box shaders are written in HLSL inside a VFX wrapper that combines vertex and pixel stages. When you save a shader it recompiles and hot-reloads automatically so you see results immediately.

Shader types

TypeDescription
Material ShadersRender objects in world space
Post Processing ShadersFull-screen effects
Compute ShadersGPU compute workloads
Create a new shader from the Asset Browser by right-clicking and selecting Create > Shader. Use VSCode with the Slang Extension for a full IDE experience including IntelliSense. Opening your project folder in VSCode prompts you to install the extension and automatically sets up file extension associations, workspace flavor, and search paths.
If IntelliSense is not working, confirm that your language mode is set to Slang and your workspace flavor is set to vfx.

Shader modes

Modes control which render passes your shader participates in. Declare them in a MODES block:
MODES
{
    Forward();
    Depth();
}
A mode can take an optional combo name or a fallback shader path:
Mode( S_MODE_COMBO )          // activates a combo when this mode runs
Mode( "mode_fallback.shader" ) // delegates to another shader for this mode
ModePurpose
DefaultCompute shaders
ForwardStandard rendering
DepthDepth prepass and shadows
ToolsShadingComplexityQuad overdraw visualisation

The Material API

Material is the contract between your shader and the lighting system. Fill its fields to describe the surface, then pass it to a shading model.
class Material
{
    float3 Albedo;
    float  Metalness;
    float  Roughness;
    float3 Emission;
    float3 Normal;           // World normal
    float  TintMask;
    float  AmbientOcclusion;
    float3 Transmission;
    float  Opacity;

    float3 WorldPosition;
    float3 WorldPositionWithOffset;
    float4 ScreenPosition;   // SV_Position
    float3 GeometricNormal;

    float3 TangentNormal;
    float3 WorldTangentU;
    float3 WorldTangentV;
    float2 LightmapUV;       // if D_BAKED_LIGHTING_FROM_LIGHTMAP
    float2 TextureCoords;    // if TOOL_VIS
};
Use Material::From( PixelInput i ) to process the standard pixel input and expose textures to the Material Editor. Use Material::Init() to start from an empty material and fill only the fields you need.

Shading models

A shading model consumes a Material and outputs the final lit colour.

ShadingModelStandard

The default Source 2 lighting model:
float4 MainPs(PixelInput i) : SV_Target0
{
    Material m = Material::From( i );
    return ShadingModelStandard::Shade( m );
}
ShadingModelStandard::Shade( Material m ) returns a float4 with the fully-lit pixel colour.

Custom shading model

Implement your own model by separating direct and indirect lighting into specular and diffuse components. The example below shows a minimal toon shader skeleton:
class ShadingModelToon
{
    static float4 Shade( Material m )
    {
        float4 color = 0;

        // Direct lighting
        for ( int i = 0; i < DynamicLight::Count(); i++ )
        {
            Light l = DynamicLight::From( ScreenPosition, WorldPosition, i );
            float3 diffuse  = /* toon diffuse */;
            float3 specular = /* toon specular */;
            color += float4(diffuse + specular, 0);
        }

        // Indirect lighting
        {
            float3 diffuse  = /* ambient */ * min( m.AmbientOcclusion,
                              ScreenSpaceAmbientOcclusion::Sample( m.ScreenPosition ) );
            float3 specular = /* envmap + fresnel */;
            color += float4(diffuse + specular, 0);
        }

        if ( DepthNormal::WantsDepthNormal() )
            return DepthNormal::Output( m );

        if ( ToolVis::WantsToolVis() )
            return ToolVis::Output( color, m );

        color.xyz = Fog::Apply( m.WorldPosition, m.ScreenPosition.xy, color );
        return color;
    }
}

Attributes and variables

Attributes pass data from C# to the GPU. Declare them by appending < Attribute("Name"); > to a variable:
bool      NoiseEnabled   < Attribute("NoiseEnabled"); >;
float4    NoiseScale     < Attribute("NoiseScale"); >;
float4x4  BoxTransform   < Attribute("BoxTransform"); >;
Texture2D NoiseTexture   < Attribute("NoiseTexture"); >;
Set them from C# with:
Attributes.Set( "Name", value );

Material Editor integration

Adding a Default value makes the attribute appear in the Material Editor UI:
float3 TintColor < UiType(Color); Default3(1.0, 1.0, 1.0) >;
AnnotationDescription
Default(arg) / Default1(arg)Sets a scalar default
Default2(a, b)Sets a float2 / int2 default
Default3(a, b, c)Sets a float3 / int3 default
Default4(a, b, c, d)Sets a float4 default
Range(arg)Clamps the UI slider
Range2(a, b)Clamps a float2 / int2 slider
Range3(a, b, c)Clamps a float3 / int3 slider
Range4(a, b, c, d)Clamps a float4 slider
UIType(type)Explicit UI widget: Slider, Color, CheckBox
DefaultFile(path)Default texture file if none is specified
Declare a texture input for the Material Editor with CreateInputTexture2D:
CreateInputTexture2D(
    AlbedoImage,
    Linear,
    8,
    "",
    "_albedo",
    "Material,10/30",
    Default( 0.5 )
);
Expose the compiled texture as a Texture2D:
Texture2D Albedo < Channel( RGBA, Box( AlbedoImage ) ); >;
Pack multiple images into a single texture by specifying channels:
Texture2D RMA < Channel( R, Box( Roughness ), Linear );
                Channel( G, Box( Metalness ), Linear );
                Channel( B, Box( Occlusion ), Linear );
                Channel( A, Box( BlendMask ), Linear ); >;

Attributes on SceneObjects

Set attributes on a specific SceneObject to affect only that object’s materials at render time:
void UpdateObject()
{
    if ( !_sceneObject.IsValid() )
        return;

    _sceneObject.Attributes.SetCombo( "D_COMBO", 1 );
    _sceneObject.Attributes.Set( "Foo", 1.0f );
    _sceneObject.Attributes.Set( "Bar", new Vector4( 1.0f, 2.0f, 3.0f, 4.0f ) );
    _sceneObject.Attributes.Set( "NoiseTexture", NoiseTexture );
}

GPU buffers

Send large blocks of data at once using constant buffers or structured buffers.
private struct Constants
{
    public Vector4 Foo;
    public Vector4 Bar;
}

private Rendering.CommandList ConstantsCommandList()
{
    Rendering.CommandList commands = new("Example Command List");

    var constants = new Constants
    {
        Foo = new Vector4( 0.0f, 1.0f, 2.0f, 3.0f ),
        Bar = new Vector4( 4.0f, 5.0f, 6.0f, 7.0f )
    };

    commands.SetConstantBuffer( "Constants", constants );
    return commands;
}

Command lists

Command lists replace the old RenderHook system. They let you schedule deferred GPU commands at a specific render stage.
public enum Stage
{
    AfterDepthPrepass  = 1000,
    AfterOpaque        = 2000,
    AfterSkybox        = 3000,
    AfterTransparent   = 4000,
    AfterViewmodel     = 5000,
    BeforePostProcess  = 6000,
    AfterPostProcess   = 7000,
    AfterUI            = 8000,
}
Attach a command list to the Camera component:
protected override void OnEnabled()
{
    commands = new Rendering.CommandList( "AmbientOcclusion" );
    commands.Set( "Foo", 1.0f );
    Camera.AddCommandList( commands, Rendering.Stage.AfterDepthPrepass );
}

protected override void OnDisabled()
{
    Camera.RemoveCommandList( commands );
    commands = null;
}
Attributes set on a command list affect the entire pipeline from that stage until the end of the frame for that view:
private Rendering.CommandList ExampleCommandList()
{
    Rendering.CommandList commands = new("Example Command List");
    var compute = new ComputeShader( "example_cs" );

    commands.Set( "NoiseScale", NoiseScale );
    commands.Set( "NoiseTexture", NoiseTexture );
    commands.DispatchCompute( compute, ExampleRenderTarget.Size );

    // Makes the result available to everything after AfterDepthPrepass
    commands.SetGlobal( "Result", ExampleRenderTarget );

    return commands;
}

Render states

Control render state anywhere outside a function block:
RenderState( BlendEnable, true );
RenderState( CullMode, NONE );
RenderState( DepthWriteEnable, false );
NameValues
FillModeWIREFRAME, SOLID
CullModeNONE, BACK, FRONT
DepthBiastrue, false
DepthClipEnabletrue, false
MultisampleEnabletrue, false
NameValues
DepthEnabletrue, false
DepthWriteEnabletrue, false
DepthFuncNEVER, LESS, EQUAL, LESS_EQUAL, GREATER, NOT_EQUAL, GREATER_EQUAL, ALWAYS
StencilEnabletrue, false
StencilReadMask0255
StencilWriteMask0255
StencilFailOpKEEP, ZERO, REPLACE, INCR_SAT, DECR_SAT, INVERT, INCR, DECR
StencilPassOpKEEP, ZERO, REPLACE, INCR_SAT, DECR_SAT, INVERT, INCR, DECR
StencilFuncNEVER, LESS, EQUAL, LESS_EQUAL, GREATER, NOT_EQUAL, GREATER_EQUAL, ALWAYS
StencilRef0255
NameValues
BlendEnabletrue, false
SrcBlendZERO, ONE, SRC_COLOR, INV_SRC_COLOR, SRC_ALPHA, INV_SRC_ALPHA, DEST_ALPHA, INV_DEST_ALPHA, DEST_COLOR, INV_DEST_COLOR, SRC_ALPHA_SAT, BLEND_FACTOR, SRC1_COLOR, INV_SRC1_COLOR, SRC1_ALPHA, INV_SRC1_ALPHA
DstBlend(same values as SrcBlend)
BlendOpADD, SUBTRACT, REV_SUBTRACT, MIN, MAX
SrcBlendAlpha(same values as SrcBlend)
DstBlendAlpha(same values as SrcBlend)
BlendOpAlphaADD, SUBTRACT, REV_SUBTRACT, MIN, MAX
ColorWriteEnable03false, R, G, B, A, RGB, RGBA, …
AlphaToCoverageEnabletrue, false
AlphaTestEnabletrue, false
AlphaTestFuncNEVER, LESS, EQUAL, LESS_EQUAL, GREATER, NOT_EQUAL, GREATER_EQUAL, ALWAYS

Sampler states

Sampler states control how textures are filtered and addressed. You can bind up to 128 textures but only 16 samplers per shader, so share samplers across textures.
Texture2D MyCoolTexture;
Texture2D MyOtherCoolTexture;
SamplerState MyPixelySampler < Filter( Point ); >;

float4 color1 = MyCoolTexture.Sample( MyPixelySampler, uv );
float4 color2 = MyOtherCoolTexture.Sample( MyPixelySampler, uv );

Sampler annotations

AnnotationDescriptionValues
FilterFiltering modePoint, Linear, Anisotropic
AddressUU addressingWrap, Mirror, Clamp, Border, Mirror_Once
AddressVV addressingWrap, Mirror, Clamp, Border, Mirror_Once
AddressWW addressingWrap, Mirror, Clamp, Border, Mirror_Once
MaxAnisoMaximum anisotropy levelInteger (e.g. 8)

Predefined samplers

SamplerDescription
g_sAnisoAnisotropic, max anisotropy 8
g_sBilinearClampBilinear, clamp UVW
g_sTrilinearWrapTrilinear, wrap UVW
g_sTrilinearClampTrilinear, clamp UVW
g_sPointClampPoint, clamp UVW
g_sPointWrapPoint, wrap UV, clamp W
Avoid the CreateTexture2D and Tex2D macros. They couple textures and samplers together in a DX9 style that limits what you can do with them.

GPU instancing

s&box instances models automatically when the renderer can batch them. You do not need to do anything for standard instancing. To access the object-to-world matrix in a vertex shader:
float3x4 CalculateInstancingObjectToWorldMatrix( const VS_INPUT i );
To access per-instance tint and skinning data:
ExtraShaderData_t GetExtraPerInstanceShaderData( const VS_INPUT i );

struct ExtraShaderData_t
{
    float4 vTint;
    uint   nBlendWeightCount; // if D_SKINNING
};
Draw thousands of instances from C# without burdening the scene system:
Graphics.DrawModelInstanced( Model, Span<Transform> transforms );

Procedural instancing

Use procedural instancing when you want to derive transforms inside the shader itself. The instance ID is available via SV_InstanceID:
PixelInput MainVs( VertexInput i, uint instanceID : SV_InstanceID )
{
    float3 offsetPosition = float3( 0, 0, 64 * instanceID );
    // ...
}
Draw procedural instances from C#:
Graphics.DrawModelInstanced( Model, count );
Graphics.DrawModelInstancedIndirect( Model, GpuBuffer );

Shader reference

Default vertex input

struct VertexInput
{
    float3 vPositionOs                  : POSITION   < Semantic( PosXyz ); >;
    float2 vTexCoord                    : TEXCOORD0  < Semantic( LowPrecisionUv ); >;
    float4 vNormalOs                    : NORMAL     < Semantic( OptionallyCompressedTangentFrame ); >;
    float4 vTangentUOs_flTangentVSign   : TANGENT    < Semantic( TangentU_SignV ); >;

    #if ( D_SKINNING > 0 )
        uint4  vBlendIndices : BLENDINDICES < Semantic( BlendIndices ); >;
        float4 vBlendWeight  : BLENDWEIGHT  < Semantic( BlendWeight ); >;
    #endif

    uint nInstanceTransformID : TEXCOORD13 < Semantic( InstanceTransformUv ); >;

    #if ( D_BAKED_LIGHTING_FROM_LIGHTMAP )
        float2 vLightmapUV : TEXCOORD3 < Semantic( LightmapUV ); >;
    #endif
}

Default pixel input

struct PixelInput
{
    float3 vPositionWithOffsetWs  : TEXCOORD0;
    float3 vNormalWs              : TEXCOORD1;
    float2 vTextureCoords         : TEXCOORD2;
    float4 vVertexColor           : TEXCOORD4;
    centroid float3 vCentroidNormalWs : TEXCOORD5;
    float3 vTangentUWs            : TEXCOORD6;
    float3 vTangentVWs            : TEXCOORD7;

    #if ( D_BAKED_LIGHTING_FROM_LIGHTMAP )
        centroid float2 vLightmapUV : TEXCOORD3;
    #endif

    float4 vPositionSs : SV_Position; // PS only
}

Global variables

TypeNameDescription
floatg_flTimeCurrent time in seconds
float4x4g_matWorldToProjectionWorld-to-projection matrix
float4x4g_matProjectionToWorldProjection-to-world matrix
float4x4g_matWorldToViewWorld-to-view matrix
float4x4g_matViewToProjectionView-to-projection matrix
float3g_vCameraPositionWsCamera position in world space
float3g_vCameraDirWsCamera direction in world space
float2g_vViewportSizeViewport dimensions
float2g_vRenderTargetSizeRender target dimensions
These names follow the older Source 2 convention. A future update may add friendlier aliases such as Time.Now or time.