Source SDK 2013: Your First Shader

From Valve Developer Community
< Zh
Jump to navigation Jump to search
English (en)中文 (zh)Translate (Translate)

Introduction

本文将会教你如何创建一个可以在Source SDK 2013(en)中使用的屏幕空间着色器。

预备知识

本教程做了如下假设:

  1. You've got a Source SDK 2013 repository checked out and setup on your local machine.
  2. You've successfully performed the steps to setup shader compiles.
  3. You understand C++.
    • If you don't, you will have a lot of trouble following along.

概览

一些术语

在往下阅读之前你应该对下面的术语有所了解:

  • 纹理(VMT): 一个文本文件,定义了一个二维面。 See the Material(en) page for more information.
  • 着色器: 用于向GPU描述它应该怎样渲染一个物体的一些指令。 See the Shader(en) page for more information.

着色器结构

在我们开始之前,对Source引擎的着色器系统有一个高屋建瓴的了解是很有帮助的。着色器系统由四个主要模块组成。下面的表格列出了这四个模块并简述了它们各自的用途:

模块名 用途
materialsystem 负责加载vmt文件并管理着色器模块。
shaderapidx9 Low level graphics handling. Contains the implementation specific (DirectX) interface to the graphics adapter. This module also stores all of the shader instances.
stdshader_dx9 Contains the base set of shaders that ship with all Source games. The _dx9 identifier indicates that this module contains only DirectX 9 versions of the built shaders. Fallback shaders will be described later in this article. The stdshader modules expose a dictionary that maps symbolic shader names to shader objects. This will be used during shader lookups which we'll cover below.
game_shader_dx9 该模块将会包含你所有的自定义着色器。在游戏初始化过程中它是最后一个加载的,使得你能够替换定义在stdshader_dx9(或更低Directex版本)里的模块。

着色器加载过程

The shader loading process is straightforward:

  1. On game startup, all stdshader_dxX modules are loaded along with the game_shader_dx9 module.
  2. Later on, something in the game triggers a call to CMaterialSystem::FindMaterial().
    • The materialsystem searches through its internal dictionary to see if it has the requested material already in memory.
      • If the material is not found, it will be loaded in from the file system and a CMaterial instance will be created and returned.
  3. The materialsystem will then call CMaterial::PrecacheVars() on the requested material.
    • CMaterial::InitializeShader() is called with the VMT's KeyValues.
      • Materialsystem loops over all loaded shader modules and checks their shader dictionaries for the shader.
        • If the shader is not found, the entire process is aborted.
        • The variables defined by the VMT are processed and loaded into the shader object.
        • Fallback shader processing occurs if required.
      • Shader reference is stored inside CMaterial instance.
      • A copy of the shader and VMT parameters is stored off inside the CMaterial instance. When the shader reference in the previous step is bound to the pipeline, these variables will be loaded in.

Parts of a shader

Source引擎的着色器包含下面的四个部分:

名称 用途
源文件(.cpp) Contains the C++ implementation of the shader. Its from here that you select the various shader combinations and perform state changes.
顶点着色器 The shader code that operates on all applicable vertices.
像素着色器 The shader code that operates on all applicable pixels.
纹理 Applied to surfaces you want your shader to render on.

Since our shader is a screenspace shader, we can leverage the SDK_Screenspace_General vertex shader. This means we'll only need to write code for the source file, the pixel shader, and the material.

源文件

Navigate to src/materialsystem/stdshaders and create a file called MyShader.cpp. Add the following contents to it:

// ----------------------------------------------------------------------------
// MYSHADER.CPP
//
// This file defines the C++ component of the example shader.
// ----------------------------------------------------------------------------

// ----------------------------------------------------------------------------
// Includes
// ----------------------------------------------------------------------------

// Must include this. Contains a bunch of macro definitions along with the
// declaration of CBaseShader.
#include "BaseVSShader.h"

// We're going to be making a screenspace effect. Therefore, we need the
// screenspace vertex shader.
#include "SDK_screenspaceeffect_vs20.inc"

// We also need to include the pixel shader for our own shader.
// Note that the shader compiler generates both 2.0 and 2.0b versions.
// Need to include both.
#include "my_pixelshader_ps20.inc"
#include "my_pixelshader_ps20b.inc"

// ----------------------------------------------------------------------------
// This macro defines the start of the shader. Effectively, every shader is
// 
// ----------------------------------------------------------------------------
BEGIN_SHADER( MyShader, "Help for my shader." )

	// ----------------------------------------------------------------------------
	// This block is where you'd define inputs that users can feed to your
	// shader.
	// ----------------------------------------------------------------------------
	BEGIN_SHADER_PARAMS
	END_SHADER_PARAMS

	// ----------------------------------------------------------------------------
	// This is the shader initialization block. This disgusting macro defines
	// a bunch of ick that makes this shader work.
	// ----------------------------------------------------------------------------
	SHADER_INIT
	{

	}

	// ----------------------------------------------------------------------------
	// We want this shader to operate on the frame buffer itself. Therefore,
	// we need to set this to true.
	// ----------------------------------------------------------------------------
	bool NeedsFullFrameBufferTexture(IMaterialVar **params, bool bCheckSpecificToThisFrame /* = true */) const
	{
		return true;
	}

	// ----------------------------------------------------------------------------
	// This block should return the name of the shader to fall back to if
	// we fail to bind this shader for any reason.
	// ----------------------------------------------------------------------------
	SHADER_FALLBACK
	{
		// Requires DX9 + above
		if (g_pHardwareConfig->GetDXSupportLevel() < 90)
		{
			Assert(0);
			return "Wireframe";
		}
		return 0;
	}

	// ----------------------------------------------------------------------------
	// This implements the guts of the shader drawing code.
	// ----------------------------------------------------------------------------
	SHADER_DRAW
	{
		// ----------------------------------------------------------------------------
		// This section is called when the shader is bound for the first time.
		// You should setup any static state variables here.
		// ----------------------------------------------------------------------------
		SHADOW_STATE
		{
			// Setup the vertex format.
			int fmt = VERTEX_POSITION;
			pShaderShadow->VertexShaderVertexFormat(fmt, 1, 0, 0);

			// We don't need to write to the depth buffer.
			pShaderShadow->EnableDepthWrites(false);

			// Precache and set the screenspace shader.
			DECLARE_STATIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
			SET_STATIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);

			// Precache and set the example shader.
			if (g_pHardwareConfig->SupportsPixelShaders_2_b())
			{
				DECLARE_STATIC_PIXEL_SHADER(my_pixelshader_ps20b);
				SET_STATIC_PIXEL_SHADER(my_pixelshader_ps20b);
			}
			else
			{
				DECLARE_STATIC_PIXEL_SHADER(my_pixelshader_ps20);
				SET_STATIC_PIXEL_SHADER(my_pixelshader_ps20);
			}
		}

		// ----------------------------------------------------------------------------
		// This section is called every frame.
		// ----------------------------------------------------------------------------
		DYNAMIC_STATE
		{
			// Use the sdk_screenspaceeffect_vs20 vertex shader.
			DECLARE_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
			SET_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);

			// Use our custom pixel shader.
			if (g_pHardwareConfig->SupportsPixelShaders_2_b())
			{
				DECLARE_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20b);
				SET_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20b);
			}
			else
			{
				DECLARE_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20);
				SET_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20);
			}
		}

		// NEVER FORGET THIS CALL! This is what actually
		// draws your shader!
		Draw();
	}

END_SHADER

Next you'll need to modify your src/materialsystem/stdshaders/game_shader_dx9_<modname>.vpc file to look something like this:

//-----------------------------------------------------------------------------
//	game_shader_dx9_<modname>.vpc
//
//	The game shader dll file.
//-----------------------------------------------------------------------------

$Macro SRCDIR	"..\.."
$Macro GAMENAME "<modname>"
$Include "$SRCDIR\materialsystem\stdshaders\game_shader_dx9_base.vpc"

$Project "Shaders (<modname>)"
{
	$Folder	"Source Files"
	{
		$File	"MyShader.cpp"
	}
}

This will add it to the build.

Pixel shader

In that same directory, you'll need to create a pixel shader file called my_pixelshader_ps2x.fxc 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"

// ------------------------------------------------------------
// 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 float4(1.0f, 0.0f, 0.0f, 1.0f);
}

Now you'll need to edit stdshader_dx9_20b.txt and add the following line: my_pixelshader_ps2x.fxc

Material

Finally, you'll need to navigate to game/<modname>/materials and create a file called my_material.vmt. It should have the following contents:

"MyShader"
{

}

Putting it all together

If you've followed the steps above, you should have created three files:

  1. The shader source file.
  2. The pixel shader itself.
  3. A material to draw the shader with.

Now all you need to do is the following:

  1. Run your build<modname>shaders.bat file.
    • Ensure it doesn't error out.
  2. Regenerate your project files with VPC and build your game shaders module.
  3. Launch your mod.
  4. Type in r_screenoverlay my_material
    • Your screen should turn red. If it does, congratulations, you've made your first shader!

Read on for the explanation of the various parts.

Why it works

This section will go through the different parts of the shader we just made.

The source file

Declaring the shader

The shader source file's first purpose is to declare your shader to the Source engine. Recall that in the shader loading process section, I mentioned how each shader module exposes a dictionary of all known shaders to the material system. The following line does this for our shader:

BEGIN_SHADER( MyShader, "Help for my shader." )

The first parameter is the name of your shader. If you actually dig into the BEGIN_SHADER macro, you'll find that this actually declares a namespace with the same name as your shader (in this case, MyShader). The last line in the shader file:

END_SHADER

Actually creates a static instance of your shader (which registers itself with the shader module) like so:

#define END_SHADER }; \
	static CShader s_ShaderInstance;\

Declaring shader parameters

Next, you'll notice the shader parameters block:

// ----------------------------------------------------------------------------
// This block is where you'd define inputs that users can feed to your
// shader.
// ----------------------------------------------------------------------------
BEGIN_SHADER_PARAMS
END_SHADER_PARAMS

This block allows you to define inputs that can be set via your material. You can create multiple materials with different parameters in order to generate different effects out of a single shader. For now, we'll leave this block empty.

Note.png注意:Even though this block is empty, its presence is required in your source file as END_SHADER_PARAM actually ends up declaring your shader class.

Requesting a full frame buffer

Initially when I wrote the code for this example shader, I wasn't able to get it to work. Turns out if you don't actually tell the shader system that you want a full framebuffer texture, the material will be rendered incorrectly if used as an overlay. To fix this, I simply put the following lines of code in:

// ----------------------------------------------------------------------------
// We want this shader to operate on the frame buffer itself. Therefore,
// we need to set this to true.
// ----------------------------------------------------------------------------
bool NeedsFullFrameBufferTexture(IMaterialVar **params, bool bCheckSpecificToThisFrame /* = true */) const
{
	return true;
}

Now, when you use the material with r_screenoverlay, it'll render your shader on a surface that's the same size as your game's window, thus rendering properly.

Defining a shader fallback

Source is well known for its ability to scale across different sets of hardware. This is due (in part) to the shader fallback system. This system allows the shader author to specify a fallback shader to use if the current shader is incompatible with the graphics hardware it is running on. For example, the Core shader for Half-Life 2: Episode One actually had three variants, DX9, DX8 and DX7. The DX9 shader would fall back to DX8 and the DX8 shader would fall back to DX7 with DX7 falling back to wireframe. The shader in this tutorial is written with only DX9 in mind so we fall back to the wireframe shader if we fail:

SHADER_FALLBACK
{
	// Requires DX9 + above
	if (g_pHardwareConfig->GetDXSupportLevel() < 90)
	{
		Assert(0);
		return "Wireframe";
	}
	return 0;
}

The drawing process

Read up on the different Shader States(en) to understand the difference between SHADOW_STATE and DYNAMIC_STATE. We'll first start with our shader's SHADOW_STATE:

// ----------------------------------------------------------------------------
// This section is called when the shader is bound for the first time.
// You should setup any static state variables here.
// ----------------------------------------------------------------------------
SHADOW_STATE
{
	// Setup the vertex format.
	int fmt = VERTEX_POSITION;
	pShaderShadow->VertexShaderVertexFormat(fmt, 1, 0, 0);

	// We don't need to write to the depth buffer.
	pShaderShadow->EnableDepthWrites(false);
 
	// Precache and set the screenspace shader.
	DECLARE_STATIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
	SET_STATIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
 
	// Precache and set the example shader.
	if (g_pHardwareConfig->SupportsPixelShaders_2_b())
	{
		DECLARE_STATIC_PIXEL_SHADER(my_pixelshader_ps20b);
		SET_STATIC_PIXEL_SHADER(my_pixelshader_ps20b);
	}
	else
	{
		DECLARE_STATIC_PIXEL_SHADER(my_pixelshader_ps20);
		SET_STATIC_PIXEL_SHADER(my_pixelshader_ps20);
	}
}

The first two lines setup the vertex format for our vertex shader. The first parameter to VertexShaderVertexFormat is a 32-bit integer containing a set of flags. These flags dictate what information we want per vertex. You can find a full listing of these flags under src/public/materialsystem/imaterial.h. In our case, we just want the vertex position. The second parameter denotes the number of texture coordinates we want (just one for our shader).

The next line (while not necessary) disables writing to the depth buffer for optimization purposes.

The DECLARE_STATIC_VERTEX_SHADER and SET_STATIC_VERTEX_SHADER lines tell the shader that we want to use sdk_screenspaceeffect_vs20 as our vertex shader. sdk_screenspaceeffect_vs20 is actually a named type that must be declared before it can be used. This is why the following lines are at the top of MyShader.cpp:

// We're going to be making a screenspace effect. Therefore, we need the
// screenspace vertex shader.
#include "SDK_screenspaceeffect_vs20.inc"

This is also the reason why you must build your shaders before building your game module. You want the latest .inc file built into it. The next set of lines do the same thing except for the pixel shader.

Note.png注意:Your vertex shader name and pixel shader name MUST match the name of their counterpart .fxc file. You'll notice that we use my_pixelshader_ps20b in the code. This is because the actual shader file is named my_pixelshader_ps2x.fxc.

The next set of lines:

// ----------------------------------------------------------------------------
// This section is called every frame.
// ----------------------------------------------------------------------------
DYNAMIC_STATE
{
	// Use the sdk_screenspace_vs20 vertex shader.
	DECLARE_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);
	SET_DYNAMIC_VERTEX_SHADER(sdk_screenspaceeffect_vs20);

	// Use our custom pixel shader.
	if (g_pHardwareConfig->SupportsPixelShaders_2_b())
	{
		DECLARE_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20b);
		SET_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20b);
	}
	else
	{
		DECLARE_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20);
		SET_DYNAMIC_PIXEL_SHADER(my_pixelshader_ps20);
	}
}

Select the dynamic combinations of the vertex and pixel shader. You can read up on shader combinations here(en).

Finally we have this line:

Draw()

This line is what actually renders your shader. It needs to go at the end of the SHADER_DRAW block.

The pixel shader

Our pixel shader is pretty straight forward. In the previous section, we set our vertex shader to be sdk_screenspace_vs20. This vertex shader passes our pixel shader a single texture coordinate. We wrap this value in the following structure:

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

Texture coordinates are a pair of (x,y) values that lie on a plane. 0,0 is the top left of the plane. 1,1 is the bottom right of the plane. The plane in this case is actually the screen itself (hence the name: screenspace).

Next, we define our main function. Every shader must have a main() function as an entry point:

// ------------------------------------------------------------
// 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 float4(1.0f, 0.0f, 0.0f, 1.0f);
}

We use the COLOR semantic at the end of main() to tell HLSL that we're returning a value that should be used as the pixel color (RGBA format). In our pixel shader's case, we're returning red.

The material

The material is super simple:

"MyShader"
{

}

The first line in a VMT file is actually the name of the shader to draw the VMT with. So when you run r_screenoverlay mymaterial, Source will render a quad the size of your screen using MyShader as the shader. You can put additional shader parameters within the brackets if you'd like. This will be covered in a subsequent tutorial.

Final thoughts

Whew! You've made it through this wall of text. By this point you should:

  1. Have some notion of the various modules responsible for shading in Source.
  2. Understand the steps that occur when you load a material that has a shader.
  3. Understand the four basic parts of a shader and what each is responsible for.
  4. Know the actions you need to take to get a custom shader into your game.

When you're ready, check out the next tutorial: Source SDK 2013: Your Second Shader(en)

练习

下面是一个简单的练习:

  1. 把着色器的颜色由红色改为黄色。
  2. 启动你的模组并在控制台输入mat_dxlevel 81。然后输入r_screenoverlay mymaterial。这时你看到的应该是一个线框矩形而不是红色,你能解释这是为什么吗?
  3. MyShader.cpp中,修改BEGIN_SHADER( MyShader, "Help for my shader." )BEGIN_SHADER( YourShader, "Help for your shader." ),然后启动游戏并试着通过r_screenoverlay命令来观察你的着色器效果,并没有正常工作,为什么呢?