这里只有一组顶点数据。多组顶点数据表示一个顶点的多个数据项。
Chapter 8: Drawing in Direct3D — Part II Overview
8.2 Shaders and the FX Framework
A vertex shader is a program executed on the graphics card's GPU (graphics processing unit) that operates on vertices. The responsibilities of a vertex shader include the world, view, and projection transformations; vertex lighting; and any other calculations that need to be done at the vertex level. Vertex shaders can output data, which can then be used as input for pixel shaders; this is done by interpolating the vertex data of a triangle across the face of the triangle during rasterization; see §6.4.8.
Note Actually, vertex shaders may also be executed in software if D3DCREATE_SOFTWARE_VERTEXPROCESSINS was specified.
Analogously, a pixel shader is a program executed on the graphics card's GPU that operates on pixel fragments (candidates that can become screen pixels if they pass various operations that can prevent a pixel from being written, such as the alpha test, stencil test, and depth buffer test). The primary responsibility of a pixel shader is to generate a color based on interpolated vertex data as input.
Note Because vertex and pixel shaders are programs we implement, there is plenty of room for creativity in devising unique graphical effects — and we'll get plenty of practice writing different kinds of shaders throughout the course of this book.
A Direct3D effect encapsulates the code that describes how to render 3D geometry in a particular way. For example, water looks much different from a car, so a water effect would encapsulate the vertex shader, pixel shader, and device states needed to render geometry that looks like water, and a separate car effect would encapsulate the vertex shader, pixel shader, and device states needed to render a car. More specifically, an effect consists of one or more techniques, which consist of one or more passes. Each pass contains a vertex shader, a pixel shader, and device state settings, which describe how to render the geometry for that particular pass. The reason for multiple passes is that some effects are implemented with multiple layers; that is, each pass renders the geometry differently and creates a layer, but then we can combine all the layers of the passes to form the net result of the rendering technique. (If you do any Photoshop work, you are probably familiar with the idea of working with several image layers and then combining them to form a net image.)
It is often desirable to have a fallback mechanism for rendering effects on different grades of graphics hardware. In other words, we want to have several versions of an effect available that implement the same effect (or attempt to implement the same effect as closely as possible) using the capabilities of different grades of hardware. This fallback mechanism is facilitated in the Direct3D effects framework by the availability of multiple techniques in an effect file; that is, each technique implements the effect by targeting different level hardware features. Then based on the game player's system specs, we select the technique for an effect that is most appropriate. For example, consider the water effect again: If we detect that the player's system has the top-of-the-line graphics card, then we would enable a very realistic, but computationally demanding, water technique. On the other hand, if we detect that the player's system has medium-level graphics hardware, we fall back to a simpler water technique. Finally, upon detecting low -grade graphics hardware, we fall back to a more crude water technique.
In addition to the fallback scheme, an effect may contain multiple techniques because the effect is complicated and requires several different techniques combined to complete the final effect. For example, we may have a technique to generate a displacement map, and then another technique that uses the displacement map to produce the final result. So this effect requires two distinct techniques used for one final result; therefore, for encapsulation purposes, you may wish to include both techniques in the same effect file.
Note Because we are writing demos and not commercial applications in this book, our effects only contain one technique; that is, we do not worry about implementing multiple techniques to support wide ranges of hardware. Furthermore, all of the effects in this book can be implemented in one pass.
Vertex and pixel shaders are programmed in a special language called the high-level shading language (HLSL). This language is very similar to C, and thus easy to learn. Our approach for teaching the HLSL and programming shaders will be example based. That is, as we progress through the book, we will introduce any new HLSL concepts that we need in order to implement the demo at hand.
We describe an effect in an .fx file, which is just a text file (like a .cpp) where we write our effects code and program our vertex and pixel shaders.
Because the .fx file is an external file, it can be modified without recompiling the main application source code; thus a game developer could update a game to take advantage of new graphics hardware by only releasing updated effect files.
8.2.1 A Simple Vertex Shader
Below is the simple vertex shader TransformVS, which we use for this chapter's demos.
uniform extern float4x4 gWVP;
struct OutputVS {
float4 posH : POSITION0;
};
OutputVS TransformVS(float3 posL : POSITION0) {
Before discussing the vertex shader, observe the structure definition before it. As you can see, HLSL structures are defined in a similar C programming language style. This structure defines the data our vertex shader outputs. In this case, we only output a single 4D vector (float4) that describes our vertex after it has been transformed into homogeneous clip space. The semantic notation, ":POSITION0" tells the GPU that the data returned in posH is a vertex position. We will see more examples of using semantics as we progress through the book and program more interesting shaders.
Now observe that the actual vertex shader is essentially a function. It contains parameters (vertex input) and returns vertex output. Conceptually, we can think of a vertex shader as a function that will be executed for each vertex we draw:
for(int i = 0; i < numVertices; ++i)
modifiedVertex[i] = vertexshader( v[i] );
So we can see that a vertex shader gives us a chance to execute some per-vertex code to do something. Note again that vertex shaders are executed on the GPU; we could always execute per-vertex operations on the CPU, but the idea is to move all graphics operations over to the graphically specialized and optimized GPU, thereby freeing the CPU for other tasks. Also note that the GPU does expect vertices, at the very least, to be transformed to homogeneous clip space;
therefore, we always do the world, view, and projection transformations.
Parameters to the vertex shader correspond to data members in our custom vertex structure (i.e., the input data). The parameter semantic ":POSITION0" tells the graphics card that this parameter corresponds to the data member in the custom vertex structure with usage D3DDECLUSASE_POSITION and index 0, as specified by the vertex declaration. So remember, in our vertex element description, we indicate for what each data member in the vertex structure is used. Then in the vertex shader, we attach a semantic to each parameter indicating a usage and index. Consequently, we now have a map that specifies how vertex structure elements get mapped to the input parameters of the vertex shader (see Figure 8.1).
Figure 8.1: A vertex structure and its associated vertex element array describing its components, and a vertex shader input parameter listing. The vertex declaration combined with the input parameter semantics define an association between the vertex structure elements and the vertex shader input parameters;
in other words, it specifies how vertex shader input parameters map to the vertex structure data members.
Note From a low -level perspective, the semantic syntax associates a variable in the shader with a hardware register. That is, the input variables are associated with input registers, and the output variables are associated with the output registers. For example, posL:POSITION0 is connected to the vertex input position register. Similarly, outvs.posH:POSITION0 is connected to the vertex output position register.
The first line of code in our vertex shader, Outputvs outvs = (Outputvs)0;, instantiates an outputvs instance, and zeros out all of its members by casting 0 to Outputvs. The next line of code performs the matrix transformation with the intrinsic HLSL mul function:
outVS.posH = mul(float4(posL, 1.0f), gWVP);
The left-hand-side argument is a 4D vector and the right-hand-side argument is a 4×4 matrix. (Observe how we augmented to homogeneous coordinates.) The function mul returns the result of the vector-matrix multiplication. Note that mul can also be used to multiply two 4×4 matrices together — mul is overloaded.
Note The code f1oat4(posL,1.0f) constructs a 4D vector in the following way: float4(posL.x,posL.y,posL.z, 1.0f) . (Observe that posL is a 3D vector — float3 .) Since we know the position of vertices are points and not vectors, we place a 1 in the fourth component (w = 1).
The variable gwvP is called an effect parameter, and it represents the combined world, view, and projection matrix. It is declared as:
uniform extern float4x4 gWVP;
The uniform keyword means that the variable does not change per vertex — it is constant for all vertices until we change it at the C++ application level. The extern keyword means that the C++ application can see the variable (i.e., the variable can be accessed outside the effect file by the C++ application code). The type f1oat4x4 is a 4×4 matrix.
Note An effect parameter is not the same as a vertex shader parameter:
uniform extern float4x4 gWVP; // < - - Effect parameter OutputVS TransformVS(
float3 post : POSITION0) // < - - Vertex shader parameter
Effect parameters are important, as they provide a means for the C++ application code to communicate with the effect. For example, the vertex shader needs to know the combined world, view, and projection matrix so that it can transform the vertices into homogeneous clip space. However, the combined matrix may change every frame (as the camera moves around the scene, for instance); thus, the C++ application code must update the effect parameter, gwvP, every frame with the current combined matrix.
After we have saved the transformed vertex into the outVS.posH data member, we are done and can return our output vertex:
return outVS;
8.2.2 A Simple Pixel Shader
Below is the simple pixel shader we use for this chapter's demos.
float4 TransformPS() : COLOR {
return float4(0.0f, 0.0f, 0.0f, 1.0f);
}
Essentially a pixel shader computes the color of a pixel fragment based on input parameters for the pixel fragment, such as interpolated vertex colors, normals, and texture coordinates. From where do pixel fragments get their input? Recall that in rasterization, certain vertex data components (colors and texture coordinates) are interpolated across the face of a triangle, thereby passing vertex data down to the pixel fragment level (i.e., the input to a pixel fragment is the interpolated vertex data for that particular pixel fragment).
Note that even though we say colors and texture coordinates, it is really more flexible than that because we can just set an arbitrary 3D vector into a 3D texture coordinate slot; likewise, we can pass general scalar information down to 1D texture coordinates. Figure 8.2 illustrates the idea.
Figure 8.2: The vertex shader output is interpolated and fed into the pixel shader as input. Note only the texture coordinate components are passed in (COLOR components are also), but the POSITION type is not; only colors and texture coordinates are fed into the pixel shader, but as said, this is more flexible than it seems because we can disguise arbitrary data as texture coordinates. For instance, in this example we pass an arbitrary scalar as a texture coordinate that won't actually be used as a texture coordinate.
A pixel shader always returns a 4D color value in the form (r, g, b, a), where r, g, b, and a are in the range [0, 1]. Consequently, a pixel shader always has a color semantic (: COLOR ) following the parameter list. (We are brief here because color is the topic of the next chapter.)
Turning our attention back to the implementation of TransformPS, observe that we have no inputs into the pixel shader (empty parameter list). This just follows from the fact that the vertex shader defined in §8.2.1 does not output any color or texture coordinate data. This will change in later chapters, but for now, we are stuck computing a color for each pixel fragment based on zero input. In other words, we can just pick any color we want. For this chapter's demos, we use black for every pixel fragment so that when we draw our wireframe lines, they will be black.
As with vertex shaders, conceptually we can think of a pixel shader as a function that will be executed for each pixel fragment:
for(int i = 0; i < numPixelFragments; ++i)
modifiedPixel Fragment[i] = pixelshader( p[i] );
So we can see that a pixel shader gives a chance to execute some per-pixel code to shade the pixel fragment.
8.2.3 A Simple FX File
Now that we have studied the vertex and pixel shaders for this chapter, we can look at the entire .fx file that describes the effect — namely rendering our geometry in wireframe mode.
OutputVS TransformVS(float3 post : POSITION0) {
// this pass.
FillMode = Wireframe;
} }
A technique is defined by the technique keyword followed by the name of the technique:
technique identifier {...}
A technique contains one or more passes (defined with the pass keyword), and each pass is given a name — we typically use the identifiers P0, P1, P2, and so on, to denote the first pass, second pass, third pass, and so on, respectively.
technique tech - identifier
Observe that a pass specifies a vertex shader, pixel shader, and any device states that need to be specified. The device state setting FillMode = Wireframe;
sets the fill mode to wireframe mode (essentially it instructs the effect to call gd3dDevice - >SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME) prior to geometry being rendered for the pass). To specify the solid fill mode, we would write:
FillMode = Solid;
There are numerous device states that can be set to modify how geometry is rendered in an effect (you already saw some of them in the Sprite demo), and we will introduce them on a need-to-know basis in this book. However, if you are curious, you can look up the complete list in the SDK documentation (from the Contents tab of the documentation, see directx/graphics/reference/d3d/enums/d3drenderstatetype.htm and
directx/graphics/reference/effectfilereference/effectfileformat/states.htm).
The modifiers vs_2_0 and ps_2_0 indicate the vertex and pixel shader version, respectively, in which to compile the shader code; here, we use vertex and pixel shader versions 2.0. To specify version 3.0, for example, we would specify vs_3_0 and ps_3_0 . Table 8.1 summarizes some differences between the major version numbers to give you an idea of their relative differences. (Additionally, there are quite a few other significant technical differences which we won't get into here, but see directx/graphics/reference/assemblylanguageshaders/vertexshaders/vertexshaderdifferences.htm and
directx/graphics/reference/assemblylanguageshaders/pixelshaders/pixelshaderdifferences.htm in the SDK documentation.) Table 8.1
Open table as spreadsheet
Property Version 1.1 Version 2.0 Version 3.0
Vertex shader constant register count At least 96 At least 256 At least 256 Vertex shader instruction slots At least 128 At least 256 At least 512
Pixel shader sampler count 4 16 16
Pixel shader instruction slots 4 texture/8 arithmetic 32 texture/64 arithmetic At least 512 (no texture instruction limit)
Note So anytime we want to draw geometry with black wireframe lines, we just use this effect. By the end of this book, we will have a nice collection of different effects that allow us to render geometry in different ways.
8.2.4 Creating an Effect
In code, an effect is represented by the lD3DXEffect interface, which we create with the following D3DX function:
HRESULT D3DXCreateEffectFromFile(
pDevice: The device to be associated with the created ID3DXEffect object.
pSrcFile: Name of the fx file that contains the effect source code we want to compile.
pDefines: This parameter is optional and we will specify null for it in this book.
plnclude: This parameter is optional and we will specify null for it in this book.
Flags: Optional flags for compiling the shaders in the effect file; specify zero for no flags. Some common options are:
D3DXSHADER_DEBUS: Instructs the compiler to write debug information.
D3DXSHADER_SKI PVALIDATION: Instructs the compiler not to do any code validation. This should only be used when you are using a shader that is known to work.
D3DXSHADER_SKIPOPTIMIZATION: Instructs the compiler not to perform any code optimization. In practice this would only be used in debugging, where you would not want the compiler to alter the code in any way.
D3DXSHADER_NO_PRESHADER: Instructs the compiler not to use preshaders — preshaders precompute constant expressions in a shader (i.e., expressions that do not change per vertex or per pixel). For example, suppose that you were multiplying two uniform matrices together in a shader. Since the matrices do not change per vertex or per pixel, there is no need to do the matrix multiplication for each vertex or for each pixel. So we can pull the computation "out of the loop" and do it once outside the shader. Preshaders do this optimization for us automatically.
D3DXSHADER_PARTIALPRECISION: Instructs the compiler to use partial precision for computations, which can improve performance on some hardware.
pPool: Optional pointer to an ID3DXEffectPool interface that is used to define how effect parameters are shared across other effect instances. In this book we will specify null for this parameter, indicating that we will not share parameters between effect files.
ppEffect: Returns a pointer to an ID3DXEffect interface representing the created effect.
ppCompi1ationErrors: Returns a pointer to an ID3DXBuffer that contains a string of error codes and messages.
Here is an example call of D3DXCreateEffectFromFile:
Note The ID3DXBuffer interface is a generic data structure that D3DX uses to store data in a contiguous block of memory. It has only two methods:
LPVOID GetBufferPointer(): Returns a pointer to the start of the data.
DWORD GetBufferSize(): Returns the size of the buffer in bytes.
To keep the structure generic it uses a void pointer. This means that it is up to us to realize the type of data being stored. For example, with D3DXCreateEffectFromFile, it is understood that the ID3DXBuffer returned from ppCompilationErrors is a Cstring. In general, the documentation of the D3DX function that returns an ID3DXBuffer will indicate the type of data being returned so that you know what to cast the data as.
We can create an empty ID3DXBuffer using the following function:
HRESULT D3DXCreateBuffer(
DWORD NumBytes, // Size of the buffer, in bytes.
LPD3DXBUFFER *ppBuffer // Returns the created buffer.
);
The following example creates a buffer that can hold four integers:
ID3DXBuffer* buffer = 0;
D3DXCreateBuffer( 4 * sizeof(int), &buffer );
8.2.5 Setting Effect Parameters
The ID3DXEffect interface provides methods for setting parameters of various types. Here is an abridged list; see the documentation for a complete list.
HRESULT ID3DXEffect::SetFloat(
D3DXHANDLE hParameter, FLOAT f);
Sets a floating-point parameter in the effect file identified by hParameter to the value f.
HRESULT ID3DXEffect::SetMatrix(
D3DXHANDLE hParameter, CONST D3DXMATRIX* pMatrix);
Sets a matrix parameter in the effect file identified by hParameter to the value pointed to by pMatrix.
HRESULT ID3DXEffect::SetTexture(
D3DXHANDLE hParameter,
LPDIRECT3DBASETEXTURE9 pTexture);
Sets a texture parameter in the effect file identified by hParameter to the value pointed to by pTexture.
HRESULT ID3DXEffect::SetVector(
D3DXHANDLE hParameter, CONST D3DXVECTOR4* pVector);
Sets a vector parameter in the effect file identified by hParameter to the value pointed to by pVector.
HRESULT ID3DXEffect::SetValue(
D3DXHANDLE hParameter, LPCVOID pData,
UINT Bytes);
Sets an arbitrary data structure parameter of size Bytes in the effect file identified by hParameter to the value pointed to by pData.
A D3DXHANDLE is just a way of identifying an internal object in an effect file (e.g., a parameter, a technique, a pass) for access; the idea is similar to using an HWND
A D3DXHANDLE is just a way of identifying an internal object in an effect file (e.g., a parameter, a technique, a pass) for access; the idea is similar to using an HWND