How to create a simple post-process shader
Posted by Wraiyth on Jul 20th, 2007
Advanced Client Side Coding.
This tutorial will take you over how to create a simple Sepia Tone shader. It assumes that you have read Introduction to Shaders in Source and have your environment configureg correctly.
[page=Your First Post-Process Shader - Pixel Shader]
Post-Process shaders are by far the easiest type of shader to create (unless you’re trying to achieve a complex effect), mainly because most of the time you won’t need to take things like lighting and shadows into account when trying to develop them. This is where we will start our first shader.
The first shader we will make is a simple Sepia Tone shader. Granted, the ability to add Sepia is already there using Valve’s Color Correction technology, but it is a relatively simple shader for beginners.
Sepia relies on a simple color manipulation that transforms the scene to black and white, then applies a tone known as 'Sepia' to the scene. This creates a yellow-ish type color, commonly associated with old films and even used to depict a dirty, unclean environment.
For Sepia, our pixel shader is what does all the work.
Create a file called 'post_sepia_ps20.fxc' in your shaders directory. This is our pixel shader for Sepia.
Full Source - post_sepia_ps20.fxc
The code here is actually relatively easy to figure out, even for non-HLSL programmers.
This include file includes a bunch of common code for pixel shaders, including functions that Valve have written for use in Half Life 2 shaders (functions like GetNormal, NormalizeWithCubemap, and DoFlashlight). Mostly, these functions are used for geometry-based shaders (applying materials to models), so you won't really be dealing with them. Its just a good habit to include it in case it is ever needed, though in this case it could probably be left out.
sampler BaseTextureSampler : register( s0 );
A sampler is pretty much a texture that is used in the shader. BaseTextureSampler is the common name for the base texture in HL2 shaders, so we'll stick to that. The code register( s0 ) registers this texture to accept input as Stage 0 - that is, when we get to coding our CPP file, we can pass external data from it at a certain stage. Each subsequent sampler thats registered needs to be a different stage.
HALF4 main(float2 iTexCoord : TEXCOORD0) : COLOR
This is our main function. Just like in C++, the type of function determines what, if anything, has to be returned by it. HALF4 (or float4) means that we must return a 4 component vector of float (x, x, x, x)
float2 iTexCoord : TEXCOORD0
This assigns the co-ordinate of the current pixel being processed (x, y) to iTexCoord. This is used later in the shader.
This created a new variable, vWeightsBW which is a 4 component vector of float. This float stores an RGB value, plus an alpha value. This code is for black and white. The values, 0.3, 0.59 and 0.11 are weighted in a way that reflect our visual system, which is more sensitive to green than other colors. Setting these values and then taking the dot product with the color of the pixel allow us to calculate the luminosity of a pixel correctly with respect to the human visual system. These values are based on standard brightness and gamma values.
Basically as above, this code is for the Sepia tone.
Another float4, this sets cColor to the color of the pixel of BaseTextureSampler at iTexCoord. tex2D is a float4, and in this case, we are using 2 float2 to fill it. BaseTextureSampler is the texture that we are using - in this case, we are using the full frame (everything you see on the screen in-game at once), and iTexCoord is the co-ordinates of the current pixel.
dot is a function to perform the dot product of 2 vectors. When we talk about float values being vectors of float, we are talking about physics vectors. cColor, which is the color of our texture co-ordinate, is multipled by the Black and White float and assigned to cTempColor
Returns the final output of the shader.
The vertex shader for this example doesn’t actually change anything. It’s the bare minimum that is required for a pixel shader.
Unlike our pixel shader, this does not ‘return’ a value. As you should know from C++ functions, a void function doesn’t return any values – this applies in shaders also.
Create a file called post_screenspaceeffect_vs20.fxc in your sdkshaders directory.
The ‘_vs20.fxc’ tells the compiler that we want to compile this shader for Vertex Shader model 2.0, and that its using HLSL code. Assembly vertex shaders use the extension ‘.vsh’
Full Shader Source – post_screenspaceeffect_vs20.fxc
This include file includes a bunch of common code for pixel shaders, including functions that Valve have written for use in Half Life 2 shaders
Basically, the entire shader gets inputs of the vertex’s position (in float4 iPosition : POSITION) and the texture co-ordinate (in float2 iTexCoord : TEXCOORD0), and then sets the input values to the output values.
More complex shaders may need to perform something in the vertex shader, but for this, nothing extra is required.
So you’re written your Sepia shader! Now you need to compile it to work.
In your stdshaders directory, create a new textfile called ‘modshaders.txt’. This is the file that you will list all your shaders in. Open it, and add these two lines:
Save and close this file.
The actual building process is rather simple. Open command prompt (Start->Run->cmd). Change to the directory where your shaders are located. There is a file in this directory called ‘buildsdkshaders.bat’ and this is the file we want to use to build the shaders.
Buildsdkshaders.bat has three parameters: a list of shaders to compile, the location of your mod, and the location of your mods source code. To compile your shaders, type this command (replacing paths appropriately)
Buildsdkshaders.bat modshaders –game C:\steam\steamapps\sourcemods\mymod –source D:\development\active\nightfall\oldsrc
A few lines containing text
Unfortunately, because the base shader code requires many other shaders to be built, you will have to generate files from the standard DX9 and DX8 shaders. Once you have correctly built your shaders, run the same command, but replacing ‘modshaders’ with ‘stdshader_dx9’ and ‘stdshader_dx8’ each time you run it. It will generate all the inc files, and then get to a point where it says ‘Compiling combo x of xxxx’. At this point, you can cancel the build because you will have all the relevant inc files.
You should have a file called ‘stdshaders_dx9.vcproj’ in your stdshaders directory. This is the project that contains all the C++ source for DX9 shaders. Open it up and add a new file called ‘post_sepia.cpp’. The source code you will need is the following.
[page=The CPP File]
Full Source Code – post_sepia.cpp
The two basic includes for any shader file. BaseVSShader is what all vertex and pixel shaders from DX8 and upwards inherit stuff
These are the files that are generated when you build the actual shaders.
BEGIN_VS_SHADER( Post_Sepia, "Help for Post_Sepia" )
This is the start of the shader. POST_SEPIA is the name of the shader, the next part is just a little tag.
SHADER_PARAM( FBTEXTURE, SHADER_PARAM_TYPE_TEXTURE, "_rt_FullFrameFB", "" )
This allows us to add any parameters we want to our shader, such as additional textures, float values to use for certain things, booleans to define wether something should be enabled/disabled etc. There are 5 params that are automatically enabled on your shader by default: COLOR, ALPHA, BASETEXTURE, FRAME, BASETEXTURETRANSFORM. For our shader, we're going to add a custom texture parameter called 'fbtexture', and set its default value to '_rt_FullFrameFB (the framebuffer).
The SHADER_INIT_PARAMS code is called right after the values for parameters are loaded from the VMT file. You could insert code in here to validate and clamp certain parameters, and to set default values for them if needed.
This can be used to detect the version of DirectX the user is running, and then set appropriate shader fallbacks to use. This feature is not in the scope of this tutorial, but is covered in a later one.
if( params[FBTEXTURE]->IsDefined() )
LoadTexture( FBTEXTURE );
The code in this block loads the shader's textures, bumpmaps, and cubemaps, and initializes its shader flags. For our shader, we're checking wether there is an FBTexture defined, and then loading it before everything else.
This contains all the state settings and code to make the shader work.
Shadow State is one of the blocks responsible for specfying all the render parameters in the shader. Shadow State is only called ONCE per material, so settings cannot be changed dynamically in this block.
pShaderShadow->EnableDepthWrites( false );
Sets wether we want to allow our shader to write to the Z buffer. This is unneeded for Sepia, so we can disable it (this line could also be left out of the code)
pShaderShadow->EnableTexture( SHADER_TEXTURE_STAGE0, true );
This enables SHADER_TEXTURE_STAGE_0 in our shader. Looking back to our pixel shader code, sampler BaseTextureSampler : register( s0 );, you can start to join everything together. s0 in the shader is automatically linked to SHADER_TEXTURE_STAGE0
int fmt = VERTEX_POSITION;
pShaderShadow->VertexShaderVertexFormat( fmt, 1, 0, 0, 0 );
This specifies which which vertex components of your FXC file needs in order to operate. Most of the time you won’t need to modify this code. For a concise explanation on this function, check out the link at the beginning of the tutorial.
DECLARE_STATIC_PIXEL_SHADER( post_sepia_ps20 );
SET_STATIC_PIXEL_SHADER( post_sepia_ps20 );
This code is new in the latest SDK update. Its pretty obvious that this code declares the pixel shader for use, and then sets the code to use this shader. The old code used pShaderShadow->SetPixelShader( "post_desat_ps20" );
DECLARE_STATIC_VERTEX_SHADER( post_screenspaceeffect_vs20 );
SET_STATIC_VERTEX_SHADER( post_screenspaceeffect_vs20 );
Again, same as above. Declares and sets the vertex shader for the code to use. The old code was pShaderShadow->SetVertexShader( "nf_screenspaceeffect_vs20", 0 );
The other block responsible for specifying render parameters. This is called everytime something with a specific material is rendered, so its parameters can be changed over time.
BindTexture( SHADER_TEXTURE_STAGE0, FBTEXTURE, -1 );
Binds the texture FBTexture to our Texture Stage 0 (as we declared in our shader and enabled earlier). The -1 is the Frame that is used, and this should always be set to -1 for most post-process shaders.
DECLARE_DYNAMIC_PIXEL_SHADER( post_sepia_ps20 );
SET_DYNAMIC_PIXEL_SHADER( post_sepia_ps20 );
DECLARE_DYNAMIC_VERTEX_SHADER( post_screenspaceeffect_vs20 );
SET_DYNAMIC_VERTEX_SHADER( post_screenspaceeffect_vs20 );
Self-explanatory. Same as the ones in the Shadow state, but instead we're using Dynamic.
Draws the shader output
Tells the system that the shader code has ended.
[page=Important Note, and the VMT]
In the latest SDK update, Valve unfortunately forgot to modify the names of several core shaders. Compiling the shader project in its current state will result in errors such as ‘game_shader_dx9.dll trying to override base shader UnlitGeneric_DX9’. To solve this, you need to ensure that the BEGIN_VS_SHADER function is preceded by SDK_ in all the core shaders. If you get an error like the one above, check that file in the project and ensure that its named correctly.
For example, BEGIN_VS_SHADER( UnlitGeneric, "Help with UnlitGeneric" ) would become BEGIN_VS_SHADER( SDK_UnlitGeneric, "Help with UnlitGeneric" )
Compile and DLL, and you’re almost there!
There are two more things we need to do.
Firstly, we need to create a material that will allow us to use the shader. Create a VMT in your materials/shaders directory (you can use any subdirectory but I personally like to use shaders). Call it post_sepia.vmt
It should look like this:
Fairly self-explanatory. We’re referencing the Post_Sepia shader (the name you use is set in the line BEGIN_VS_SHADER( Post_Sepia, "Help for Post_Sepia" ) ), and we want to send FBTexture _rt_FullFrameFB, which is our framebuffer (the entire screen image for that frame).
[page=Client DLL Code]
The last step we need is a simple way to toggle the shader on and off. We don’t want to have to type r_screenoverlay shaders/post_sepia everytime to enable it, and then r_screenoverlay NULL to disable it.
Luckily, we have a rather simple method to turn it on and off using ConVars. Open up view_scene.cpp
Find the line
static ConVar r_screenfademaxsize( "r_screenfademaxsize", "0" );
Below it, add
ConVar post_sepia("post_sepia", "0", 0, "Enable/Disable Sepia shader");
The post_sepia ConVar is what we are going to use to toggle it on and off. Post_sepia 1 will turn the shader on, and post_sepia 0 will turn it off.
The best way that I’ve found to apply post process effects is by using a custom functions that will give us the ability to handle all the effects pretty easily. Open viewrender.h and at the very bottom, add
void PerformPostProcessEffects( int x, int y, int width, int height );
This will be the function that will perform all of our custom post-process effects (because we’ll be adding some in the future).
Go back to view_scene.cpp. At the bottom of the RenderView() function, add this line
PerformPostProcessEffects( view.x, view.y, view.width, view.height );
This will send all appropriate parameters to the function that we need to apply the full screen shaders.
Now its time to actually add the function. The basic function looks like this:
The entire function is pretty self-explanatory. Firstly, we make sure that there is an effect enabled. If there aren’t any active effects, then we want to kill the function or else we’re wasting precious resources.
Next, assign our material to a pointer called pSepiaMat. Then we check wether the user is at DX8 or higher. If he is, we continue.
You may wonder why there is a second check for post_sepia.GetInt() in the next line. This is the ‘framework’ for multiple effects. The initial check I use for checking all ConVars (if I have multiple shaders) and then subsequently check the ConVars once again to see which effects I actually have to draw. So that’s the purpose of the second check, along with verifying that pSepiaMat is a valid material.
We then call the DrawScreenEffectMaterial, which draws pSepiaMat over the entire screen (x, y, width, height is what we get passed from RenderView())
And that’s it! This is all you need for your shader. Simply compile, open up your mod and type post_sepia 1 into the console.
Now have a go at creating a shader yourself. Another simple one to use is color inverse, which can be done with one line of HLSL code:
float4 cColor=1-tex2D( BaseTextureSampler, iTexCoord );
Using Sepia as a reference, there’s not much that needs changing until you hit the code for the DLL.
Thats it for this tutorial. It was a rather long winded one, but now you're capable of creating simple shaders. With a bit of playing around, you'll soon be able to create some great effects. Make sure that you check out the nVidia and ATI SDKs for a bunch of great shader samples
Tutorial originally posted on Wraiyths Website