Source SDK 2013: Your First Shader
Contents
Introduction
This page will teach you how to create a screen space pixel shader for use with Source SDK 2013.
Prerequisites
This tutorial assumes the following:
- You've got a Source SDK 2013 repository checked out and setup on your local machine.
- If you don't, visit the Source SDK 2013 page.
- You've successfully performed the steps to setup shader compiles.
- If not, visit the Source SDK 2013: Shader Authoring page.
- You understand C++.
- If you don't, you will have a lot of trouble following along.
Overview
Definitions
Words you should understand the meaning of before you continue reading:
- Material (or VMT): A text file that defines a two dimensional surface. See the Material page for more information.
- Shader: A set of coded instructions which are sent to a GPU that dictate how an object should be drawn. See the Shader page for more information.
Architecture
Before we can begin, it is helpful to have a high-level understanding of the shader system in Source. The shader system consists of 4 major modules. The table below lists each module and describes (in a simplified fashion) its purpose:
Module name | Purpose |
---|---|
materialsystem | Responsible for loading vmt files and managing shader modules. |
shaderapidx9 | Low level graphics handling. Contains the implementation specific (DirectX) interface to the graphics adapter. This module also stores all of the shader instances. |
stdshader_dx9 | Contains the base set of shaders that ship with all Source games. The _dx9 identifier indicates that this module contains only DirectX 9 versions of the built shaders. Fallback shaders will be described later in this article. The stdshader modules expose a dictionary that maps symbolic shader names to shader objects. This will be used during shader lookups which we'll cover below. |
game_shader_dx9 | This module will contain all of your custom shaders. It is last to load in the game initialization process, thus allowing you to replace shaders defined in stdshader_dx9 (or below). |
Shader Loading Process
The shader loading process is straightforward:
- On game startup, all stdshader_dxX modules are loaded along with the game_shader_dx9 module.
- Later on, something in the game triggers a call to CMaterialSystem::FindMaterial().
- The materialsystem searches through its internal dictionary to see if it has the requested material already in memory.
- If the material is not found, it will be loaded in from the file system and a CMaterial instance will be created and returned.
- The materialsystem searches through its internal dictionary to see if it has the requested material already in memory.
- The materialsystem will then call CMaterial::PrecacheVars() on the requested material.
- CMaterial::InitializeShader() is called with the VMT's KeyValues.
- Materialsystem loops over all loaded shader modules and checks their shader dictionaries for the shader.
- If the shader is not found, the entire process is aborted.
- The variables defined by the VMT are processed and loaded into the shader object.
- Fallback shader processing occurs if required.
- Shader reference is stored inside CMaterial instance.
- A copy of the shader and VMT parameters is stored off inside the CMaterial instance. When the shader reference in the previous step is bound to the pipeline, these variables will be loaded in.
- Materialsystem loops over all loaded shader modules and checks their shader dictionaries for the shader.
- CMaterial::InitializeShader() is called with the VMT's KeyValues.
Parts of a Shader
A shader in source consists of four parts:
Name | Purpose |
---|---|
Source file (.cpp) | Contains the C++ implementation of the shader. Its from here that you select the various shader combinations and perform state changes. |
Vertex shader | The shader code that operates on all applicable vertices. |
Pixel shader | The shader code that operates on all applicable pixels. |
Material | Applied to surfaces you want your shader to render on. |
Since our shader is a screenspace shader, we can leverage the SDK_Screenspace_General vertex shader. This means we'll only need to write code for the source file, the pixel shader, and the material.
Source File
Navigate to src/materialsystem/stdshaders
and create a file called MyShader.cpp
. Add the following contents to it:
// ----------------------------------------------------------------------------
// MYSHADER.CPP
//
// This file defines the C++ component of the example shader.
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// Includes
// ----------------------------------------------------------------------------
// Must include this. Contains a bunch of macro definitions along with the
// declaration of CBaseShader.
#include "BaseVSShader.h"
// We're going to be making a screenspace effect. Therefore, we need the
// screenspace vertex shader.
#include "SDK_screenspaceeffect_vs20.inc"
// We also need to include the pixel shader for our own shader.
// Note that the shader compiler generates both 2.0 and 2.0b versions.
// Need to include both.
#include "my_pixelshader_ps20.inc"
#include "my_pixelshader_ps20b.inc"
// ----------------------------------------------------------------------------
// This macro defines the start of the shader. Effectively, every shader is
//
// ----------------------------------------------------------------------------
BEGIN_SHADER( MyShader, "Help for my shader." )
// ----------------------------------------------------------------------------
// This block is where you'd define inputs that users can feed to your
// shader.
// ----------------------------------------------------------------------------
BEGIN_SHADER_PARAMS
END_SHADER_PARAMS
// ----------------------------------------------------------------------------
// This is the shader initialization block. This disgusting macro defines
// a bunch of ick that makes this shader work.
// ----------------------------------------------------------------------------
SHADER_INIT
{
}
// ----------------------------------------------------------------------------
// We want this shader to operate on the frame buffer itself. Therefore,
// we need to set this to true.
// ----------------------------------------------------------------------------
bool NeedsFullFrameBufferTexture(IMaterialVar **params, bool bCheckSpecificToThisFrame /* = true */) const
{
return true;
}
// ----------------------------------------------------------------------------
// This block should return the name of the shader to fall back to if
// we fail to bind this shader for any reason.
// ----------------------------------------------------------------------------
SHADER_FALLBACK
{
// Requires DX9 + above
if (g_pHardwareConfig->GetDXSupportLevel() < 90)
{
Assert(0);
return "Wireframe";
}
return 0;
}
// ----------------------------------------------------------------------------
// This implements the guts of the shader drawing code.
// ----------------------------------------------------------------------------
SHADER_DRAW
{
// ----------------------------------------------------------------------------
// This section is called when the shader is bound for the first time.
// You should setup any static state variables here.
// ----------------------------------------------------------------------------
SHADOW_STATE
{
// Setup the vertex format.
int fmt = VERTEX_POSITION;
pShaderShadow->VertexShaderVertexFormat(fmt, 1, 0, 0);
// We don't need to write to the depth buffer.
pShaderShadow->EnableDepthWrites(false);
// Precache and set the screenspace shader.
DECLARE_STATIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
SET_STATIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
// Precache and set the example shader.
if (g_pHardwareConfig->SupportsPixelShaders_2_b())
{
DECLARE_STATIC_PIXEL_SHADER(my_pixelshader_ps20b);
SET_STATIC_PIXEL_SHADER(my_pixelshader_ps20b);
}
else
{
DECLARE_STATIC_PIXEL_SHADER(my_pixelshader_ps20);
SET_STATIC_PIXEL_SHADER(my_pixelshader_ps20);
}
}
// ----------------------------------------------------------------------------
// This section is called every frame.
// ----------------------------------------------------------------------------
DYNAMIC_STATE
{
// Use the sdk_screenspaceeffect_vs20 vertex shader.
DECLARE_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
SET_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
// Use our custom pixel shader.
if (g_pHardwareConfig->SupportsPixelShaders_2_b())
{
DECLARE_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20b);
SET_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20b);
}
else
{
DECLARE_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20);
SET_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20);
}
}
// NEVER FORGET THIS CALL! This is what actually
// draws your shader!
Draw();
}
END_SHADER
Next you'll need to modify your src/materialsystem/stdshaders/game_shader_dx9_<modname>.vpc
file to look something like this:
//----------------------------------------------------------------------------- // game_shader_dx9_<modname>.vpc // // The game shader dll file. //----------------------------------------------------------------------------- $Macro SRCDIR "..\.." $Macro GAMENAME "<modname>" $Include "$SRCDIR\materialsystem\stdshaders\game_shader_dx9_base.vpc" $Project "Shaders (<modname>)" { $Folder "Source Files" { $File "MyShader.cpp" } }
This will add it to the build.
Pixel Shader
In that same directory, you'll need to create a pixel shader file called my_pixelshader_ps2x.fxc
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"
// ------------------------------------------------------------
// 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 float4(1.0f, 0.0f, 0.0f, 1.0f);
}
Now you'll need to edit stdshader_dx9_20b.txt
and add the following line:
my_pixelshader_ps2x.fxc
Material
Finally, you'll need to navigate to game/<modname>/materials
and create a file called my_material.vmt
. It should have the following contents:
"MyShader" { }
Putting It All Together
If you've followed the steps above, you should have created three files:
- The shader source file.
- The pixel shader itself.
- A material to draw the shader with.
Now all you need to do is the following:
- Run your build<modname>shaders.bat file.
- Ensure it doesn't error out.
- Regenerate your project files with VPC and build your game shaders module.
- Launch your mod.
- Type in
r_screenoverlay my_material
- Your screen should turn red. If it does, congratulations, you've made your first shader!
Read on for the explanation of the various parts.
Why It Works
This section will go through the different parts of the shader we just made.
The Source File
Declaring the Shader
The shader source file's first purpose is to declare your shader to the Source engine. Recall that in the shader loading process section, I mentioned how each shader module exposes a dictionary of all known shaders to the material system. The following line does this for our shader:
BEGIN_SHADER( MyShader, "Help for my shader." )
The first parameter is the name of your shader. If you actually dig into the BEGIN_SHADER macro, you'll find that this actually declares a namespace with the same name as your shader (in this case, MyShader
). The last line in the shader file:
END_SHADER
Actually creates a static instance of your shader (which registers itself with the shader module) like so:
#define END_SHADER }; \
static CShader s_ShaderInstance;\
Declaring Shader Parameters
Next, you'll notice the shader parameters block:
// ----------------------------------------------------------------------------
// This block is where you'd define inputs that users can feed to your
// shader.
// ----------------------------------------------------------------------------
BEGIN_SHADER_PARAMS
END_SHADER_PARAMS
This block allows you to define inputs that can be set via your material. You can create multiple materials with different parameters in order to generate different effects out of a single shader. For now, we'll leave this block empty.
END_SHADER_PARAM
actually ends up declaring your shader class.Requesting a Full Frame Buffer
Initially when I wrote the code for this example shader, I wasn't able to get it to work. Turns out if you don't actually tell the shader system that you want a full framebuffer texture, the material will be rendered incorrectly if used as an overlay. To fix this, I simply put the following lines of code in:
// ----------------------------------------------------------------------------
// We want this shader to operate on the frame buffer itself. Therefore,
// we need to set this to true.
// ----------------------------------------------------------------------------
bool NeedsFullFrameBufferTexture(IMaterialVar **params, bool bCheckSpecificToThisFrame /* = true */) const
{
return true;
}
Now, when you use the material with r_screenoverlay, it'll render your shader on a surface that's the same size as your game's window, thus rendering properly.
Defining a Shader Fallback
Source is well known for its ability to scale across different sets of hardware. This is due (in part) to the shader fallback system. This system allows the shader author to specify a fallback shader to use if the current shader is incompatible with the graphics hardware it is running on. For example, the Core shader for Half-Life 2: Episode One actually had three variants, DX9, DX8 and DX7. The DX9 shader would fall back to DX8 and the DX8 shader would fall back to DX7 with DX7 falling back to wireframe. The shader in this tutorial is written with only DX9 in mind so we fall back to the wireframe shader if we fail:
SHADER_FALLBACK
{
// Requires DX9 + above
if (g_pHardwareConfig->GetDXSupportLevel() < 90)
{
Assert(0);
return "Wireframe";
}
return 0;
}
The Drawing Process
Read up on the different Shader States to understand the difference between SHADOW_STATE
and DYNAMIC_STATE
. We'll first start with our shader's SHADOW_STATE
:
// ----------------------------------------------------------------------------
// This section is called when the shader is bound for the first time.
// You should setup any static state variables here.
// ----------------------------------------------------------------------------
SHADOW_STATE
{
// Setup the vertex format.
int fmt = VERTEX_POSITION;
pShaderShadow->VertexShaderVertexFormat(fmt, 1, 0, 0);
// We don't need to write to the depth buffer.
pShaderShadow->EnableDepthWrites(false);
// Precache and set the screenspace shader.
DECLARE_STATIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
SET_STATIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
// Precache and set the example shader.
if (g_pHardwareConfig->SupportsPixelShaders_2_b())
{
DECLARE_STATIC_PIXEL_SHADER(my_pixelshader_ps20b);
SET_STATIC_PIXEL_SHADER(my_pixelshader_ps20b);
}
else
{
DECLARE_STATIC_PIXEL_SHADER(my_pixelshader_ps20);
SET_STATIC_PIXEL_SHADER(my_pixelshader_ps20);
}
}
The first two lines setup the vertex format for our vertex shader. The first parameter to VertexShaderVertexFormat
is a 32-bit integer containing a set of flags. These flags dictate what information we want per vertex. You can find a full listing of these flags under src/public/materialsystem/imaterial.h
. In our case, we just want the vertex position. The second parameter denotes the number of texture coordinates we want (just one for our shader).
The next line (while not necessary) disables writing to the depth buffer for optimization purposes.
The DECLARE_STATIC_VERTEX_SHADER
and SET_STATIC_VERTEX_SHADER
lines tell the shader that we want to use sdk_screenspaceeffect_vs20
as our vertex shader. sdk_screenspaceeffect_vs20
is actually a named type that must be declared before it can be used. This is why the following lines are at the top of MyShader.cpp
:
// We're going to be making a screenspace effect. Therefore, we need the
// screenspace vertex shader.
#include "SDK_screenspaceeffect_vs20.inc"
This is also the reason why you must build your shaders before building your game module. You want the latest .inc
file built into it. The next set of lines do the same thing except for the pixel shader.
.fxc
file. You'll notice that we use my_pixelshader_ps20b
in the code. This is because the actual shader file is named my_pixelshader_ps2x.fxc
.The next set of lines:
// ----------------------------------------------------------------------------
// This section is called every frame.
// ----------------------------------------------------------------------------
DYNAMIC_STATE
{
// Use the sdk_screenspace_vs20 vertex shader.
DECLARE_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
SET_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
// Use our custom pixel shader.
if (g_pHardwareConfig->SupportsPixelShaders_2_b())
{
DECLARE_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20b);
SET_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20b);
}
else
{
DECLARE_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20);
SET_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20);
}
}
Select the dynamic combinations of the vertex and pixel shader. You can read up on shader combinations here.
Finally we have this line:
Draw()
This line is what actually renders your shader. It needs to go at the end of the SHADER_DRAW
block.
The Pixel Shader
Our pixel shader is pretty straight forward. In the previous section, we set our vertex shader to be sdk_screenspace_vs20
. This vertex shader passes our pixel shader a single texture coordinate. We wrap this value in the following structure:
// ------------------------------------------------------------
// 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;
};
Texture coordinates are a pair of (x,y) values that lie on a plane. 0,0 is the top left of the plane. 1,1 is the bottom right of the plane. The plane in this case is actually the screen itself (hence the name: screenspace).
Next, we define our main function. Every shader must have a main() function as an entry point:
// ------------------------------------------------------------
// 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 float4(1.0f, 0.0f, 0.0f, 1.0f);
}
We use the COLOR
semantic at the end of main()
to tell HLSL that we're returning a value that should be used as the pixel color (RGBA format). In our pixel shader's case, we're returning red.
The Material
The material is super simple:
"MyShader" { }
The first line in a VMT file is actually the name of the shader to draw the VMT with. So when you run r_screenoverlay mymaterial
, Source will render a quad the size of your screen using MyShader
as the shader. You can put additional shader parameters within the brackets if you'd like. This will be covered in a subsequent tutorial.
Final Thoughts
Whew! You've made it through this wall of text. By this point you should:
- Have some notion of the various modules responsible for shading in Source.
- Understand the steps that occur when you load a material that has a shader.
- Understand the four basic parts of a shader and what each is responsible for.
- Know the actions you need to take to get a custom shader into your game.
When you're ready, check out the next tutorial: Source SDK 2013: Your Second Shader
Exercises
Here are some simple exercises you can do:
- Change the color that is drawn by the shader from red to yellow.
- Launch your mod and type
mat_dxlevel 81
into the console. Then type inr_screenoverlay mymaterial
. Rather than displaying red, you'll see a wireframe rectangle. Can you explain why? - In
MyShader.cpp
, changeBEGIN_SHADER( MyShader, "Help for my shader." )
toBEGIN_SHADER( YourShader, "Help for your shader." )
. Then launch the game and attempt to view your shader via ther_screenoverlay
command. It doesn't work. Why?