DEV Community

Kirill GPRB
Kirill GPRB

Posted on • Edited on

Source Engine materials

Sorse
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"
}
Enter fullscreen mode Exit fullscreen mode

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.
Texture list utility

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

Oh yeah it's all coming together!

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.
A diagram

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!

Top comments (0)