Source Engine materials
Kirill GPRB
Posted on December 3, 2020
I can see a lack of Source Engine articles nowadays and some people asked me to write an article explaining how materials work, so...
This article assumes you know C++ and shaders at certain level.
Materials
What is a material in Source Engine?
The simplest answer would be a bunch of abstraction layers on KeyValues that is used as a base for all rendering activity.
Materials are stored on disk and being loaded into the memory on demand (e.g. loading a map, pre-caching a model) or pre-cached as a client screen space effects.
Surface properties
Materials may have a special variable that is being used by the physics and the sound to determine how does this material behave in relation with other objects in the world.
Relativity to the shaders
Each and every material basically is a recursive key-value pair where the key contains a shader's name and the value contains material variables to use in the C++ part of the shader.
To get a better understanding how it works, let's look at a simple material example:
// This is a shader's name.
// This means that we will use a shader named LightmappedGeneric
// in order to get a model to be shaded by a pre-baked lightmap
LightmappedGeneric
{
// This is called a material variable, a key-value pair
// that is declared inside C++ code and being used by it in
// order to modify shader's behaviour.
$basetexture "props/citymap001"
}
Shaders
HLSL
A shader program is a program in the first place, therefore it has to be compiled. Source uses FXC (DXC's older brother) to pre-compile shaders.
There's only two shaders you would use in Source:
- A vertex shader. This shader contains some code that does all that matrix math allowing you to see a 3D picture on a 2D screen. The shader's entry point is called for each vertex.
- A pixel shader. This shader contains some code that affects the actual appearance of a vertex: its color, transparency, etc. The shader's entry point is called for each pixel of a render target.
C++
Any program should be glued to the material system so both the game and Hammer Editor draw materials correctly. Each shader is defined in a separate source code file due to some insanity going on with macros. Default shaders such as LightmappedGeneric
"live" in the stdshader_dx9.dll
module while the game-defined shaders "live" in the game_shader_dx9.dll
module.
I will keep the shader's C++ code away from this article because this article attempts to explain how the material system works in general and not how to make your own shader.
Textures
Like the shaders, textures are also defined by a string identifier and can be re-loaded like materials.
Textures are stored on disk in a format called VTF (stands for Valve Texture Format) and being loaded whenever the material system decides to.
Render targets
Render targets (or framebuffers) in Source are stored in the same way as regular textures are. The main difference is the naming convention (the _rt_
prefix for all RTs) and a flag indicating that the texture is actually a render target.
Drawing a mesh
CViewSetup
The renderer uses a special thing to set up the view and projection matrices; this thing is called a ViewSetup and contains basically all the information required to generate a matrix procedurally: frame size, FOV, camera position, view angles, etc.
The views are stored in the stack, allowing programmer to define their own views without affecting previously set views.
Game developer's view
To draw something, game developer just calls a bunch of rendering context methods to prepare, draw, and then return the renderer to its previous state. The Source SDK code (basically the Half-Life 2 and Episodic source code) contains some functions that are made to simplify screen-space effect drawing process: those functions do almost everything leaving you only to find a material and pass it to the function.
Engine developer's view
When the drawing call is being invoked, the renderer firstly searches for the shader by the name retrieved from the material.
If the shader exists, the shader draw part is being invoked. In this part shader pre-caches HLSL binaries if required, parses some material variables and fills the shader constants and samplers with the parsed data, then binds the HLSL binaries.
Finally, the renderer draw a mesh using the bound shader.
Conclusion
I hope you have learned something new about how does Source Engine renders stuff. In the future, I would like to write an article about actually making a post-processing shader for a game.
Useful links
- Valve Developer Community (VDC), (click)
- Source SDK 2013 code on GitHub (click)
- My Source 2013 SP mod source code (click)
Corrections
After writing this article and working on shaders for a while I just suddenly realized that C++ part of shaders is not actually bound with "shader" definition: that's all just a pipeline of a data-driven renderer!
Posted on December 3, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.