Source SDK 2013: Your Second Shader

From Valve Developer Community
Jump to: navigation, search

Introduction

This page will teach you the basics of passing parameters to your shaders.

Prerequisites

This tutorial assumes the following:

  1. You've followed all of the instructions on the Source SDK 2013: Your First Shader page.
    • And you've been able to successfully build the shader source code on that page.

Overview

What are shader parameters?

Shader parameters are bits of data that you can feed into a shader from a Valve Material File. It turns out that VMT files are actually KeyValues which you can add additional data to. The following snippet is from materials/dev/lumcompare.vmt:

"screenspace_general"
{
    "$PIXSHADER"            "luminance_compare_ps20"
    "$BASETEXTURE"          "_rt_FullFrameFB"
    "$ALPHATESTED"          "1"
    "$DISABLE_COLOR_WRITES" "1"
}

Recall back to the first tutorial. The first line in a VMT file is the name of the shader to draw that material with. The subsequent lines that begin with $ however, are known as material parameters. In this example, there are four parameters, each of which are associated with a value.

You might be asking yourself how we know what the parameters of a given shader are. Unfortunately, there isn't a simple in-game command (as far as the author is aware of) that lists them for you. You'll need to take a look at the source code of the shader. Luckily, we have the source code for screenspace_general. The parameters it can handle are listed in its BEGIN_SHADER_PARAMS block:

Note:For Source SDK 2013, the screenspace_general code lives under src/materialsystem/stdshaders/screenspace_general.cpp.
SHADER_PARAM( C0_X,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C0_Y,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C0_Z,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C0_W,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C1_X,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C1_Y,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C1_Z,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C1_W,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C2_X,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C2_Y,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C2_Z,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C2_W,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C3_X,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C3_Y,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C3_Z,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( C3_W,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( PIXSHADER, SHADER_PARAM_TYPE_STRING, "", "Name of the pixel shader to use" )
SHADER_PARAM( DISABLE_COLOR_WRITES,SHADER_PARAM_TYPE_INTEGER,"0","")
SHADER_PARAM( ALPHATESTED,SHADER_PARAM_TYPE_FLOAT,"0","")
SHADER_PARAM( TEXTURE1, SHADER_PARAM_TYPE_TEXTURE, "", "" )
SHADER_PARAM( TEXTURE2, SHADER_PARAM_TYPE_TEXTURE, "", "" )
SHADER_PARAM( TEXTURE3, SHADER_PARAM_TYPE_TEXTURE, "", "" )
SHADER_PARAM( LINEARREAD_BASETEXTURE, SHADER_PARAM_TYPE_INTEGER, "0", "" )
SHADER_PARAM( LINEARREAD_TEXTURE1, SHADER_PARAM_TYPE_INTEGER, "0", "" )
SHADER_PARAM( LINEARREAD_TEXTURE2, SHADER_PARAM_TYPE_INTEGER, "0", "" )
SHADER_PARAM( LINEARREAD_TEXTURE3, SHADER_PARAM_TYPE_INTEGER, "0", "" )
SHADER_PARAM( LINEARWRITE,SHADER_PARAM_TYPE_INTEGER,"0","")
SHADER_PARAM( X360APPCHOOSER, SHADER_PARAM_TYPE_INTEGER, "0", "Needed for movies in 360 launcher" )

The next section will cover each piece of the SHADER_PARAM macro.

Understanding SHADER_PARAM

The SHADER_PARAM macro has the following definition:

#define SHADER_PARAM( param, paramtype, paramdefault, paramhelp ) \
	static CShaderParam param( "$" #param, paramtype, paramdefault, paramhelp, 0 );
Note:Essentially, every shader parameter you create with this macro gets written as a static variable in memory. When your shader is loaded into the game, each shader parameter adds itself to a static vector called s_ShaderParams, which was created in the BEGIN_SHADER macro. The materialsystem uses this vector to load in data from VMT files.

The macro itself is pretty straight forward. The first parameter is the name you want your shader parameter to have. You'll need to use this name in your VMT file in order to pass data to your shader. In the VMT example above, we had the following material parameter:

"$PIXSHADER"            "luminance_compare_ps20"

In order for this to work, the corresponding shader parameter needs to have the same name (which it does):

SHADER_PARAM( PIXSHADER, SHADER_PARAM_TYPE_STRING, "", "Name of the pixel shader to use" )

This is a nice segue into shader parameter types.

Shader parameter types

As you can tell by the code snippet above, there are multiple types of data you can pass to a shader. Eleven of them to be exact. You can find a list of all supported data types under src/public/materialsystem/imaterialsystem.h. As of the time of this writing (12/27/2014), the types are as follows: The following table explains each possible shader parameter type and how to use it:

Parameter type Explanation Usage
SHADER_PARAM_TYPE_TEXTURE A texture reference. Pass in the name of the texture you want to bind (e.g. _rt_FullFrameFB).
SHADER_PARAM_TYPE_INTEGER An integer value. Pass in a scalar value (e.g. 0 or 5).
SHADER_PARAM_TYPE_COLOR A color value. Pass in an RGBA color value (e.g. [0 255 152 255]). Note, the brackets are required.
SHADER_PARAM_TYPE_VEC2 A two component vector. Pass in a vector2d (e.g. [0.0 1.0]). Note, the brackets are required.
SHADER_PARAM_TYPE_VEC3 A three component vector. Pass in a vector3d (e.g. [0.0 1.0 0.5]). Note, the brackets are required.
SHADER_PARAM_TYPE_VEC4 A four component vector. Pass in a vector4d (e.g. [0.0 1.0 0.5 0.4]). Note, the brackets are required.
SHADER_PARAM_TYPE_FLOAT A floating point value. Pass in a scalar float (e.g. 0.4).
SHADER_PARAM_TYPE_BOOL A boolean value. Pass in 0 for false and 1 for true.
SHADER_PARAM_TYPE_FOURCC Unknown. Appears as if this denotes custom data that isn't a regular data type. Unknown.
SHADER_PARAM_TYPE_MATRIX Allows you to define a transformation matrix or a 4x4 regular matrix. If you want to define a transformation matrix, you need to use the following value: center a b scale c d rotate e translate f g. Replace a-g with values from 0..1. If you want to define a regular matrix, you will need to put sixteen floating point values within brackets as such: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
SHADER_PARAM_TYPE_MATERIAL A material. Pass in the path to a VMT (e.g. dev/lumcompare.vmt).
SHADER_PARAM_TYPE_STRING A string. Pass in a string (e.g. "Hello").

Usage

Now that we've covered what shader parameters are and how to actually define them, let's add one to our shader from the previous tutorial. We'll start simple for this tutorial and create a shader parameter that takes in an RGBA color value.

Adding the parameter to the C++ code

Open up src/materialsystem/stdshaders/MyShader.cpp. Change your BEGIN_SHADER_PARAMS block to look like the following:

// ----------------------------------------------------------------------------
// This block is where you'd define inputs that users can feed to your
// shader.
// ----------------------------------------------------------------------------
BEGIN_SHADER_PARAMS
	SHADER_PARAM(MYCOLOR, SHADER_PARAM_TYPE_COLOR, "[0 0 0 0]", "What color to render with.")
END_SHADER_PARAMS

In Source, all of your custom shader parameters need to go between the BEGIN_SHADER_PARAMS block and the END_SHADER_PARAMS block. You'll note that for our example, our shader parameter will be referred to using the name MYCOLOR.

Note:The parameter name actually ends up getting stringified and has a '$' character prepended to it after the macro is expanded.

The third argument, [0 0 0 0] is the default value to give to the shader parameter if one is not provided. The final argument is a help string that describes what that parameter does.

Validating the shader parameter

The CBaseShader class provides you with the ability to validate parameters passed into your shader. To opt into this functionality, simply add in a SHADER_INIT_PARAMS block right after your END_SHADER_PARAMS line:

	// ----------------------------------------------------------------------------
// Validates the parameters passed to the shader by the VMT.
// ----------------------------------------------------------------------------
SHADER_INIT_PARAMS()
{
	// Ensure we've recieved a color.
	if( !params[MYCOLOR]->IsDefined() )
	{
		// Set to black error color.
		params[MYCOLOR]->SetVecValue( 0.0f, 0.0f, 0.0f, 1.0f );
	}
}

You can use the IMaterialVar::IsDefined() method to figure out if a variable is present in a VMT. If it isn't, it returns false. In the example above, we're checking for the existence of $MYCOLOR within the VMT. If it's not there, we're forcing it to default to black.

Binding the variable to a shader register

Now that we've received and validated the shader parameter, we need to actually push its value to the shader itself. To do this, we make use of shader registers. You can do this using the IShaderAPI::SetPixelShaderConstant method. For our example, place this code right before your DECLARE_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20); call:

// Pixel shader constant register 0 will contain the
// color passed in by the material.
float c0[4];
params[MYCOLOR]->GetVecValue( c0, 4 );
pShaderAPI->SetPixelShaderConstant( 0, c0, ARRAYSIZE( c0 ) / 4 );

This code performs the following steps:

  1. Retrieve the 4 component color value from the given shader variable.
  2. Bind the value to shader constant register zero.
    • Note:Since it's a constant, the register is 'c0' (for constant 0). Keep in mind that this is a value that consists of four floats. This becomes relevant in the next section.

Receiving the value in the shader

Now that we've hooked up the shader parameter in the C++ code, we need to actually account for it in the pixel shader code. Open up src/materialsystem/stdshaders/my_pixelshader_ps2x.fxc and replace it 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"

// ------------------------------------------------------------
// Pixel shader registers. These are enabled and set by the
// shader source file.
// ------------------------------------------------------------

// This is the shader constant from the VMT file.
const float4 gColor : register( c0 );

// ------------------------------------------------------------
// 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 gColor;
}

The line where we actually retrieve the constant from register zero is this one:

// This is the shader constant from the VMT file.
const float4 gColor : register( c0 );

We then take this value and return it as the color for every pixel while the shader is active.

Updating the material

The material change is simple; we just need to add in a value for $MYCOLOR. Open up game/<modname>/materials/mymaterial.vmt and replace it with the following contents:

"MyShader"
{
	"$MYCOLOR" "[0 1 1 1]"
}

The next time this material is loaded, the materialsystem will read out $MYCOLOR and pass it down to our shader.

Tying it all together

Now that you've hooked up the shader parameter to both the C++ code and the pixel shader code, you're ready to build. Perform the following steps:

  1. Run your build<modname>shaders.bat file.
  2. Build your game_shader_dx9_<modname> project.
  3. Launch your game and run r_screenoverlay mymaterial.
    • You should see a cyan color appear. If so, you've successfully completed this tutorial. Way to go!

Exercises

  1. Attempt to change the color drawn without modifying either the C++ code or the shader itself.
    • Note:The command mat_reloadallmaterials will come in handy for this.
  2. Add in a second shader parameter called MYCOLOR2 of type COLOR and hook it up to your shader and material. Have your pixel shader add the two colors together and return the result. Can you do it?
  3. How would you prevent someone from passing in a color with a red value greater than zero? What code would you modify?