Source SDK 2013: Your Second Shader
Contents
Introduction
This page will teach you the basics of passing parameters to your shaders.
Prerequisites
This tutorial assumes the following:
- You've followed all of the instructions on the Source SDK 2013: Your First Shader page.
- And you've been able to successfully build the shader source code on that page.
Overview
What are shader parameters?
Shader parameters are bits of data that you can feed into a shader from a Valve Material File. It turns out that VMT files are actually KeyValues which you can add additional data to. The following snippet is from materials/dev/lumcompare.vmt
:
"screenspace_general" { "$PIXSHADER" "luminance_compare_ps20" "$BASETEXTURE" "_rt_FullFrameFB" "$ALPHATESTED" "1" "$DISABLE_COLOR_WRITES" "1" }
Recall back to the first tutorial. The first line in a VMT file is the name of the shader to draw that material with. The subsequent lines that begin with $
however, are known as material parameters. In this example, there are four parameters, each of which are associated with a value.
You might be asking yourself how we know what the parameters of a given shader are. Unfortunately, there isn't a simple in-game command (as far as the author is aware of) that lists them for you. You'll need to take a look at the source code of the shader. Luckily, we have the source code for screenspace_general. The parameters it can handle are listed in its BEGIN_SHADER_PARAMS
block:
src/materialsystem/stdshaders/screenspace_general.cpp
.SHADER_PARAM( C0_X,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C0_Y,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C0_Z,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C0_W,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C1_X,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C1_Y,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C1_Z,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C1_W,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C2_X,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C2_Y,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C2_Z,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C2_W,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C3_X,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C3_Y,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C3_Z,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C3_W,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( PIXSHADER, SHADER_PARAM_TYPE_STRING, "", "Name of the pixel shader to use" )
SHADER_PARAM( DISABLE_COLOR_WRITES,SHADER_PARAM_TYPE_INTEGER,"0","")
SHADER_PARAM( ALPHATESTED,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( TEXTURE1, SHADER_PARAM_TYPE_TEXTURE, "", "" )
SHADER_PARAM( TEXTURE2, SHADER_PARAM_TYPE_TEXTURE, "", "" )
SHADER_PARAM( TEXTURE3, SHADER_PARAM_TYPE_TEXTURE, "", "" )
SHADER_PARAM( LINEARREAD_BASETEXTURE, SHADER_PARAM_TYPE_INTEGER, "0", "" )
SHADER_PARAM( LINEARREAD_TEXTURE1, SHADER_PARAM_TYPE_INTEGER, "0", "" )
SHADER_PARAM( LINEARREAD_TEXTURE2, SHADER_PARAM_TYPE_INTEGER, "0", "" )
SHADER_PARAM( LINEARREAD_TEXTURE3, SHADER_PARAM_TYPE_INTEGER, "0", "" )
SHADER_PARAM( LINEARWRITE,SHADER_PARAM_TYPE_INTEGER,"0","")
SHADER_PARAM( X360APPCHOOSER, SHADER_PARAM_TYPE_INTEGER, "0", "Needed for movies in 360 launcher" )
The next section will cover each piece of the SHADER_PARAM
macro.
Understanding SHADER_PARAM
The SHADER_PARAM
macro has the following definition:
#define SHADER_PARAM( param, paramtype, paramdefault, paramhelp ) \
static CShaderParam param( "$" #param, paramtype, paramdefault, paramhelp, 0 );
s_ShaderParams
, which was created in the BEGIN_SHADER
macro. The materialsystem uses this vector to load in data from VMT files.The macro itself is pretty straight forward. The first parameter is the name you want your shader parameter to have. You'll need to use this name in your VMT file in order to pass data to your shader. In the VMT example above, we had the following material parameter:
"$PIXSHADER" "luminance_compare_ps20"
In order for this to work, the corresponding shader parameter needs to have the same name (which it does):
SHADER_PARAM( PIXSHADER, SHADER_PARAM_TYPE_STRING, "", "Name of the pixel shader to use" )
This is a nice segue into shader parameter types.
Shader parameter types
As you can tell by the code snippet above, there are multiple types of data you can pass to a shader. Eleven of them to be exact. You can find a list of all supported data types under src/public/materialsystem/imaterialsystem.h
. As of the time of this writing (12/27/2014), the types are as follows:
The following table explains each possible shader parameter type and how to use it:
Parameter type | Explanation | Usage |
---|---|---|
SHADER_PARAM_TYPE_TEXTURE | A texture reference. | Pass in the name of the texture you want to bind (e.g. _rt_FullFrameFB ).
|
SHADER_PARAM_TYPE_INTEGER | An integer value. | Pass in a scalar value (e.g. 0 or 5 ).
|
SHADER_PARAM_TYPE_COLOR | A color value. | Pass in an RGBA color value (e.g. [0 255 152 255] ). Note, the brackets are required.
|
SHADER_PARAM_TYPE_VEC2 | A two component vector. | Pass in a vector2d (e.g. [0.0 1.0] ). Note, the brackets are required.
|
SHADER_PARAM_TYPE_VEC3 | A three component vector. | Pass in a vector3d (e.g. [0.0 1.0 0.5] ). Note, the brackets are required.
|
SHADER_PARAM_TYPE_VEC4 | A four component vector. | Pass in a vector4d (e.g. [0.0 1.0 0.5 0.4] ). Note, the brackets are required.
|
SHADER_PARAM_TYPE_FLOAT | A floating point value. | Pass in a scalar float (e.g. 0.4 ).
|
SHADER_PARAM_TYPE_BOOL | A boolean value. | Pass in 0 for false and 1 for true.
|
SHADER_PARAM_TYPE_FOURCC | Unknown. Appears as if this denotes custom data that isn't a regular data type. | Unknown. |
SHADER_PARAM_TYPE_MATRIX | Allows you to define a transformation matrix or a 4x4 regular matrix. | If you want to define a transformation matrix, you need to use the following value: center a b scale c d rotate e translate f g . Replace a-g with values from 0..1. If you want to define a regular matrix, you will need to put sixteen floating point values within brackets as such: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
SHADER_PARAM_TYPE_MATERIAL | A material. | Pass in the path to a VMT (e.g. dev/lumcompare.vmt ).
|
SHADER_PARAM_TYPE_STRING | A string. | Pass in a string (e.g. "Hello" ).
|
Usage
Now that we've covered what shader parameters are and how to actually define them, let's add one to our shader from the previous tutorial. We'll start simple for this tutorial and create a shader parameter that takes in an RGBA color value.
Adding the parameter to the C++ code
Open up src/materialsystem/stdshaders/MyShader.cpp
. Change your BEGIN_SHADER_PARAMS
block to look like the following:
// ----------------------------------------------------------------------------
// This block is where you'd define inputs that users can feed to your
// shader.
// ----------------------------------------------------------------------------
BEGIN_SHADER_PARAMS
SHADER_PARAM(MYCOLOR, SHADER_PARAM_TYPE_COLOR, "[0 0 0 0]", "What color to render with.")
END_SHADER_PARAMS
In Source, all of your custom shader parameters need to go between the BEGIN_SHADER_PARAMS
block and the END_SHADER_PARAMS
block. You'll note that for our example, our shader parameter will be referred to using the name MYCOLOR
.
'$'
character prepended to it after the macro is expanded.The third argument, [0 0 0 0]
is the default value to give to the shader parameter if one is not provided. The final argument is a help string that describes what that parameter does.
Validating the shader parameter
The CBaseShader
class provides you with the ability to validate parameters passed into your shader. To opt into this functionality, simply add in a SHADER_INIT_PARAMS
block right after your END_SHADER_PARAMS
line:
// ----------------------------------------------------------------------------
// Validates the parameters passed to the shader by the VMT.
// ----------------------------------------------------------------------------
SHADER_INIT_PARAMS()
{
// Ensure we've recieved a color.
if( !params[MYCOLOR]->IsDefined() )
{
// Set to black error color.
params[MYCOLOR]->SetVecValue( 0.0f, 0.0f, 0.0f, 1.0f );
}
}
You can use the IMaterialVar::IsDefined()
method to figure out if a variable is present in a VMT. If it isn't, it returns false
. In the example above, we're checking for the existence of $MYCOLOR
within the VMT. If it's not there, we're forcing it to default to black.
Binding the variable to a shader register
Now that we've received and validated the shader parameter, we need to actually push its value to the shader itself. To do this, we make use of shader registers. You can do this using the IShaderAPI::SetPixelShaderConstant
method. For our example, place this code right before your DECLARE_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
call:
// Pixel shader constant register 0 will contain the
// color passed in by the material.
float c0[4];
params[MYCOLOR]->GetVecValue( c0, 4 );
pShaderAPI->SetPixelShaderConstant( 0, c0, ARRAYSIZE( c0 ) / 4 );
This code performs the following steps:
- Retrieve the 4 component color value from the given shader variable.
- Bind the value to shader constant register zero.
- Note:Since it's a constant, the register is 'c0' (for constant 0). Keep in mind that this is a value that consists of four floats. This becomes relevant in the next section.
Receiving the value in the shader
Now that we've hooked up the shader parameter in the C++ code, we need to actually account for it in the pixel shader code. Open up src/materialsystem/stdshaders/my_pixelshader_ps2x.fxc
and replace it with the following contents:
// ------------------------------------------------------------
// MY_PIXELSHADER_PS2X.FXC
//
// This file implements an extremely simple pixel shader for
// the Source Engine.
// ------------------------------------------------------------
// ------------------------------------------------------------
// Includes
// ------------------------------------------------------------
// This is the standard include file that all pixel shaders
// should have.
#include "common_ps_fxc.h"
// ------------------------------------------------------------
// Pixel shader registers. These are enabled and set by the
// shader source file.
// ------------------------------------------------------------
// This is the shader constant from the VMT file.
const float4 gColor : register( c0 );
// ------------------------------------------------------------
// This structure defines what inputs we expect to the shader.
// These will come from the SDK_screenspace_general vertex
// shader.
//
// For now, all we care about is what texture coordinate to
// sample from.
// ------------------------------------------------------------
struct PS_INPUT
{
float2 texCoord : TEXCOORD0;
};
// ------------------------------------------------------------
// This is the main entry point to our pixel shader. We'll
// return the color red as an output.
// ------------------------------------------------------------
float4 main( PS_INPUT i ) : COLOR
{
return gColor;
}
The line where we actually retrieve the constant from register zero is this one:
// This is the shader constant from the VMT file. const float4 gColor : register( c0 );
We then take this value and return it as the color for every pixel while the shader is active.
Updating the material
The material change is simple; we just need to add in a value for $MYCOLOR
. Open up game/<modname>/materials/mymaterial.vmt
and replace it with the following contents:
"MyShader" { "$MYCOLOR" "[0 1 1 1]" }
The next time this material is loaded, the materialsystem will read out $MYCOLOR
and pass it down to our shader.
Tying it all together
Now that you've hooked up the shader parameter to both the C++ code and the pixel shader code, you're ready to build. Perform the following steps:
- Run your
build<modname>shaders.bat
file. - Build your game_shader_dx9_<modname> project.
- Launch your game and run
r_screenoverlay mymaterial
.- You should see a cyan color appear. If so, you've successfully completed this tutorial. Way to go!
Exercises
- Attempt to change the color drawn without modifying either the C++ code or the shader itself.
- Note:The command
mat_reloadallmaterials
will come in handy for this.
- Add in a second shader parameter called
MYCOLOR2
of typeCOLOR
and hook it up to your shader and material. Have your pixel shader add the two colors together and return the result. Can you do it? - How would you prevent someone from passing in a color with a red value greater than zero? What code would you modify?