Post tutorial Report RSS Post-Process Shader

How to create a simple post-process shader

Posted by on - Advanced Client Side Coding

[page=Introduction]
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

#include "common_ps_fxc.h" 
 
sampler BaseTextureSampler : register( s0 ); 
 
HALF4 main(float2 iTexCoord : TEXCOORD0) : COLOR 
{  
 float4 vWeightsBW=float4(0.3,0.59,0.11,0); 
 float4 vWeightsSepia=float4(0.9,0.7,0.3,1); 
  
 float4 cColor=tex2D(BaseTextureSampler,iTexCoord); 
 float4 cTempColor=dot(cColor,vWeightsBW);  
 return dot(cTempColor,vWeightsSepia); 
} 

The code here is actually relatively easy to figure out, even for non-HLSL programmers.

#include "common_ps_fxc.h"
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.

float4 vWeightsBW=float4(0.3,0.59,0.11,0);
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.

float4 vWeightsSepia=float4(0.9,0.7,0.3,1);
Basically as above, this code is for the Sepia tone.

float4 cColor=tex2D(BaseTextureSampler,iTexCoord);
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.

float4 cTempColor=dot(cColor,vWeightsBW);
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

return dot(cTempColor,vWeightsSepia);
Returns the final output of the shader.

[page=Vertex 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

#include "common_vs_fxc.h" 
void main( in float4 iPosition : POSITION, in float2 iTexCoord : TEXCOORD0, out float4 oPosition   : POSITION, out float2 oTexCoord   : TEXCOORD0) 
{  
 oPosition=iPosition; 
 oTexCoord=iTexCoord; 
} 

#include "common_ps_fxc.h"
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.

[page=Compiling]
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:
Post_sepia_ps20.fxc
Post_screenspaceeffect_vs20.fxc
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

#include "BaseVSShader.h"  
#include "post_sepia_ps20.inc" 
#include "post_screenspaceeffect_vs20.inc" 
 
BEGIN_VS_SHADER( Post_Sepia, "Help for Post_Sepia" ) 
 
 BEGIN_SHADER_PARAMS 
   SHADER_PARAM( FBTEXTURE, SHADER_PARAM_TYPE_TEXTURE, "_rt_FullFrameFB", "" ) 
 END_SHADER_PARAMS 
 
 // Set up anything that is necessary to make decisions in SHADER_FALLBACK. 
 SHADER_INIT_PARAMS() 
 { 
 } 
 
 SHADER_FALLBACK 
 { 
  return 0; 
 } 
 
 SHADER_INIT 
 { 
  if( params[FBTEXTURE]->IsDefined() ) 
  { 
   LoadTexture( FBTEXTURE ); 
  } 
 } 
 
 SHADER_DRAW 
 { 
  SHADOW_STATE 
  { 
   pShaderShadow->EnableDepthWrites( false ); 
 
   pShaderShadow->EnableTexture( SHADER_TEXTURE_STAGE0, true ); 
   int fmt = VERTEX_POSITION; 
   pShaderShadow->VertexShaderVertexFormat( fmt, 1, 0, 0, 0 ); 
 
   DECLARE_STATIC_PIXEL_SHADER( post_sepia_ps20 ); 
   SET_STATIC_PIXEL_SHADER( post_sepia_ps20 ); 
 
   DECLARE_STATIC_VERTEX_SHADER( post_screenspaceeffect_vs20 ); 
   SET_STATIC_VERTEX_SHADER( post_screenspaceeffect_vs20 ); 
 
  } 
  DYNAMIC_STATE 
  { 
   BindTexture( SHADER_TEXTURE_STAGE0, FBTEXTURE, -1 ); 
   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 ); 
  } 
  Draw(); 
 } 
END_SHADER 

#include "BaseVSShader.h"
The two basic includes for any shader file. BaseVSShader is what all vertex and pixel shaders from DX8 and upwards inherit stuff

#include "post_sepia_ps20.inc"
#include "post_screenspaceeffect_vs20.inc"

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.

BEGIN_SHADER_PARAMS
SHADER_PARAM( FBTEXTURE, SHADER_PARAM_TYPE_TEXTURE, "_rt_FullFrameFB", "" )
END_SHADER_PARAMS

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).

SHADER_INIT_PARAMS()
{
}

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.

SHADER_FALLBACK
{
return 0;
}

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.

SHADER_INIT
{
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.

SHADER_DRAW
{
}

This contains all the state settings and code to make the shader work.

SHADOW_STATE
{
}

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 );

DYNAMIC_STATE
{
}

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.

Draw();
Draws the shader output

END_SHADER
Tells the system that the shader code has ended.

[page=Important Note, and the VMT]
IMPORTANT NOTE
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:
"Post_Sepia "
{
"$fbtexture" "_rt_FullFrameFB"
}

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:

void CViewRender::PerformPostProcessEffects( int x, int y, int width, int height ) 
{ 
 if ( (post_sepia.GetInt() == 0) ) //no active effects, bail out 
  return; 
 
 IMaterial *pSepiaMat = materials->FindMaterial( "shaders/post_sepia", TEXTURE_GROUP_CLIENT_EFFECTS, true ); 
   
 if ( g_pMaterialSystemHardwareConfig->GetDXSupportLevel() >= 80 ) 
 { 
 
  if ( (post_sepia.GetInt() == 1) && pSepiaMat ) 
  { 
            DrawScreenEffectMaterial( pSepiaMat, x, y, width, height ); 
  } 
 } 
} 

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.

[page=Conclusion]
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

Post comment Comments
Alf_Hari
Alf_Hari

For someone who's never gone near shaders before, I found it a very interesting and enlightening tutorial. Can't wait to try it out when I get a copy of Source.

Cheers

Reply Good karma Bad karma+1 vote
Wraiyth Author
Wraiyth

Congratulations to anyone who actually read this through all the way. Its a long tute :P

Reply Good karma+1 vote
Ging
Ging

Because that doesn't teach you how to write a custom shader for use in Source... Which is well, sort of the point to this tutorial...

Reply Good karma Bad karma+1 vote
Varsity
Varsity

Why not use colour correction?

Reply Good karma Bad karma-3 votes
Jyffeh
Jyffeh

:thumbup:

Reply Good karma Bad karma+1 vote
Wraiyth Author
Wraiyth

Varsity, I wrote this tutorial long before Color Correction existed in source. And Ging is right - the point of the tutorial is to teach you how to make a shader, not how to add Sepia. Its the concepts & application that matters, not the actual effect that is produced, I could have just as easily done a funky blur shader, downsample, or something like that.

Reply Good karma+1 vote
TKAzA
TKAzA

Well done wra nice tuts mate.

Reply Good karma Bad karma+1 vote
Shilano_TG
Shilano_TG

I have tried this, but when I run the bat file using the command line shadercompile.exe 'ecounters a problem'.

I thought this may be down to the latest sdk update seems to make the sdk orange box... So I had to use the engine ep1 fix. So I tried editing the batch file so that it went to bin/ep1/bin instead of bin. But this didn't help.

Any Ideas why shadercompile.exe keeps bombing out?

Reply Good karma Bad karma0 votes
Davias
Davias

Hi,I had the same problem, I solved it copying all files and folders from sourcesdk/bin/orangebox/bin into sourcesdk/bin. (You can replace the folders filters, phonomeextractors, ect. because they are empty.)
It seems that Valve hasn't updated the buildsdkshaders.bat and the compiling scripts for the orange box.
Maybe you have to copy all files from sourcesdk/bin/ep1/bin but then I encountered a problem.

Maybe someone can help me, becuase I have another problem. Compiling works fine but when I run Half-Life console says that Post_Sepia is a unknown shader so I think there's a problem with the game_shader_dx9.dll.

Any ideas?

Reply Good karma Bad karma+1 vote
neptar
neptar

Davias,

Copying the files stopped it from bombing out, but there was another message, something to do with mysql, which doesn't make sense. I think it could possibly be a problem with perl.

When I activate the shader in game in teh console it says Post_Seia is an unkown shader just the same as yours and i get a white line across the screen.

Reply Good karma Bad karma+1 vote
Zambi
Zambi

I don't exactly know if this will help this long after, BUT in the post_sepia .vmt file provided, the is a space after Post_Sepia inside the quotes that must be removed otherwise it will throw out that error.

Reply Good karma Bad karma+1 vote
Wraiyth Author
Wraiyth

Well, either thats failing, or the shader isn't copying correctly. Check your shaders/fxc folder to make sure that its there, with the SDK screwing up its possible that shaders aren't copying correctly. If they exist then its almost definitely an issue with the DLL, you could throw some DevMsgs in there to see whats going on but aside from that, not sure if theres much you could really do. Theres been alot of problems since the SDK changes :(

Reply Good karma+1 vote
Omnituens
Omnituens

I too am having an issue, "cant open mysql_wrapper.dll!"

I cant find a solution anywhere.

Reply Good karma Bad karma+1 vote
Shilano_TG
Shilano_TG

Exactly the same.... If anybody knows the solution to the mysql_wrapper.dll error, please post a comment!

Reply Good karma Bad karma+1 vote
hyphen-ated
hyphen-ated

Here's mysql_wrapper.dll: Filedropper.com
put it in <account>/sourcesdk/bin

Reply Good karma Bad karma+1 vote
Bumbi
Bumbi

Great Tutorial :)
To use the ep1/bin-dir you need to edit both buildsdkshaders.bat and runvmpi.pl (line 54).
Don't understand why they didn't integrated that into OrangeBox-Mods yet (?)

--Bumbi

P.S. Is it a general bug that I see the HTML Special Chars in the Code-Parts above escaped?

Reply Good karma Bad karma+1 vote
Actsplosive
Actsplosive

Can someone put the mysql_wrapper.dll file on Gamefront.com or similar free file sharing site. Filedropper.com seems to want payment now to get any files.

Thanks,
M

Reply Good karma Bad karma+1 vote
hyphen-ated
hyphen-ated

Here's mysql_wrapper.dll rehosted Files.gamefront.com

Reply Good karma Bad karma+1 vote
MrMattWebb
MrMattWebb

wall of text

Reply Good karma Bad karma0 votes
error3972
error3972

Great Tutorial!

Reply Good karma Bad karma+2 votes
Guest
Guest

So once I have the shader, how do I apply it to the game? how do I make it appear in the game?
sorry for the stupid question, I'm new to this...

Reply Good karma Bad karma+1 vote
B4RR3l
B4RR3l

any idea how to bypass the 1P layers and apply reshader to the game layer after 1P? I would love to make MXAO+ and DOF working in this game

Reply Good karma Bad karma+1 vote
Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: