Shader authoring/Anatomy of a Shader/Combo Variables

From Valve Developer Community
Jump to: navigation, search

Combo Variables

The second parameter to IShaderShadow::SetVertexShader and IShaderShadow::SetPixelShader is the shader index, which is how the Source engine allows you to have conditionals inside your shaders. HLSL currently does not support branching code. It supports if() statements, but only for static conditionals (usually based on #defines - things that can't change after the shader has been compiled).

Without dynamic if() statements in shaders, it's very easy to wind up having to hand-code every variation of a shader, and you can end up with a lot of manual labor. For example, if you have a shader that has an option to multiply the color from a third texture after it multiplies the base texture and lightmap, you'd have to copy the contents of your pixel shader into a separate .FXC file, add the code to multiply the third texture, and choose that file with pShaderShadow->SetPixelShader. That's with 1 option. With 10 orthogonal options, you'd have 2^10 (1024) shader files you'd have to manage.

To simulate dynamic conditionals in HLSL code, the Source engine uses a special syntax in its .FXC files to denote combo variables which can be different across different executions of the same .FXC file. Under the hood, the engine does the dirty work of precompiling 2^n variations of the .FXC file, each with different #defines used for the if() statements.

To make a combo variable, add a line like this to the top of your .FXC file:

// STATIC: "VERTEXCOLOR" "0..1"

or this:

// DYNAMIC: "VERTEXCOLOR" "0..1"

where VERTEXCOLOR is replaced with the name of the variable. Use STATIC whenever possible - static combo variables can only be set once per material (at load time). The values of the static combo variables are set inside the SHADOW_STATE block in your shader's C++ class.

If you need to dynamically change the value of the combo variables (based on a proxy), use DYNAMIC. The values of DYNAMIC combo variables are set inside the DYNAMIC_STATE part of your shader's C++ class.

Note: The '0..1' part following the shader can be any range of values (like '5..10'). Be aware that the number of permutations (and thus memory usage and runtime overhead) rapidly increases as you add more possible dynamic shader variable values. For example, these lines:

// STATIC: "STATIC_VAR_1" "0..1"
// STATIC: "STATIC_VAR_2" "0..1"
// STATIC: "STATIC_VAR_3" "0..1"
// STATIC: "STATIC_VAR_4" "0..1" 

would cause 16 different versions of your .FXC file to be compiled (2^4 or one for each possible combination of valid values).

This, however:

// STATIC: "STATIC_VAR_1" "0..9"
// STATIC: "STATIC_VAR_2" "0..9"
// STATIC: "STATIC_VAR_3" "0..9"
// STATIC: "STATIC_VAR_4" "0..9" 

would cause 10,000 (10^4) different versions of your .FXC file to be compiled, which will not only take a long time to compile, it will also eat up lots of memory. You can use SKIP statements to optimize this.

Important - shader include files

When you compile your .FXC code, a file called fxctmp9\[FXC filename].inc is created. This file contains classes that you use to access the combo variables. For an example, if you look at src\sdkshaders\sdk_lightmap.cpp, it has this include statement at the top:

#include "sdk_lightmap_vs20.inc" 

That include file defines two classes. One is for the combo variables inside a SHADOW_STATE block (combo variables denoted with // STATIC in the .FXC file), and the other is for combo variables specified inside a DYNAMIC_STATE block (denoted with // DYNAMIC inside the .FXC file).

The names of the two classes defined inside a shader include (.inc) file are [shader name]_Static_Index and [shader name]_Dynamic_Index. To tie this all together, let's say you were writing a vertex shader in a file called MyVertexShader.fxc, and it had some STATIC and DYNAMIC combo variables at the top of your .FXC file that looked like this:

// STATIC: "STATIC_VAR_1" "0..1"
// STATIC: "STATIC_VAR_2" "0..1"

// DYNAMIC: "DYNAMIC_VAR_1" "0..1"
// DYNAMIC: "DYNAMIC_VAR_2" "0..1" 

Then, at the top of your .cpp file for your shader, you would have this line:

#include "MyVertexShader.inc" 

Then, inside your SHADOW_STATE block, where you would say pShaderShadow->SetVertexShader, you would write this code:

MyVertexShader_Static_Index vshIndex;

vshIndex.SetSTATIC_VAR_1( 0 or 1 );
vshIndex.SetSTATIC_VAR_2( 0 or 1 );
pShaderShadow->SetVertexShader( "MyVertexShader", vshIndex.GetIndex() );

and inside your DYNAMIC_STATE block, you would write this code.

MyVertexShader_Dynamic_Index vshIndex;

vshIndex.SetDYNAMIC_VAR_1( 0 or 1 );
vshIndex.SetDYNAMIC_VAR_2( 0 or 1 );
pShaderAPI->SetVertexShaderIndex( vshIndex.GetIndex() ); 

SKIP Statements

SKIP statements can be used to tell Source's shader compiler about values of combo variables that will never be used. For example, if you had 3 combo variables:

// STATIC: "STATIC_VAR_1" "0..1"
// STATIC: "STATIC_VAR_2" "0..1"
// STATIC: "STATIC_VAR_3" "0..1" 

but you knew that STATIC_VAR_1 and STATIC_VAR_2 could never both have a value of 1 at the same time, you would add a SKIP statement like this:

// SKIP: $STATIC_VAR_1 && $STATIC_VAR_2 

Then the compiler wouldn't have to bother compiling and storing the two extra combinations of variables.

Valve's LightmappedGeneric shader saves thousands of unnecessary versions of its .FXC file by using SKIP statements.