|
Chapter 6: HLSL Introduction
6 HLSL IntroductionTired of writing shaders in assembly language? Try the high-level shader language (HLSL). Microsoft DirectX 9 contains the first release of a C-like shading language for developing vertex, pixel, and procedural texture shaders. This C-like shader language is in addition to the assembly-language shader capability that can be used to generate vertex shaders, pixel shaders, and effects, beginning with DirectX 8.HLSL supports the development of shaders from C-like functions. The language supports many standard language features such as functions, expressions, statements, standard data types, user-designed data types, and preprocessor directives. The goal of this chapter is to get you writing HLSL programs right away, without getting bogged down by rules, exceptions, and special cases. This chapter introduces you to three working code samples that demonstrate basic vertex, pixel, and texture shaders. The examples are generally concise but functional, and they'll serve to get you ready to generate shaders almost immediately. HLSL supports shader instructions that are written similar to mathematical expressions. Like other graphics languages, the mathematical expressions take full advantage of vector and matrix math. The language follows many C rules and introduces a few other rules specific to HLSL that make shader programming more intuitive and compact. Compared to similar assembly-language instructions, HLSL instructions are easier to read and can almost be debugged just by looking at the source code. In this chapter, all the high-level shaders are compiled using D3DXCompileShaderxxx APIs. Assembly-language shaders are assembled from the D3DXAssembleShaderxxx APIs. Effects can include assembly-language and high-level language shaders. After we cover HLSL, we'll see how effects provide a powerful framework for managing pipeline state. Effects use the ID3DXEffectCompiler interface to compile shaders, which are shown in Part 3. After you've seen the tutorials in this chapter, you'll have a good introduction to shader writing with HLSL. The next two chapters extend these tutorials by going into detail about major functional areas of the language such as data types, expressions, shader semantics, functions, and custom data types. Included are examples that demonstrate a glow shader and a metallic paint shader using vertex shaders, pixel shaders, and procedural textures. So without further delay, let's jump into the first shader.
Tutorial 1: Start with a Vertex Shader: Hello WorldIn computer programs, the simplest program is often one line of code that displays the string "Hello World." This tutorial is analogous in that it implements a vertex shader that contains one line of code. The vertex shader transforms the vertices of one triangle (very simple geometry) from model space to projection space. Once transformed, the triangle is rendered in the 3-D scene. Here's the vertex shader:
float4x4 WorldViewProj; This shader contains a global variable declaration and a function. The variable WorldViewProj is a 4x4 matrix. Each member of the matrix is a floating-point value.
float4x4 WorldViewProj; WorldViewProj will be initialized by the application to contain the product of the world, view, and projection transformation matrices. The matrix will be used to transform the vertices, just like the vertex transformation done in the fixed function pipeline. The VertexShader_Tutorial_1 function is made up of a function declaration:
float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION and the body of the function:
{The function declaration identifies the return type float4, the function name VertexShader_Tutorial_1, and the input argument list (float4 inPos : POSITION ). The return value has a semantic named POSITION. This argument list contains one argument. It's a float4 type named inPos, and it contains a semantic also named POSITION. All of this looks very similar to C, except for the data type and the semantics. (There is no float4 native type in C.) The float4 type is a four-component vector that contains four floating-point components. HLSL has several vector and matrix types to help with the vector and 3-D math operations. For more information about data types and vector math, see Chapter 7. Semantics are an additional feature of HLSL. They were added to make binding data between shaders and the pipeline easier. Semantics can bind vertex data to vertex shader registers, vertex shader outputs to pixel shader inputs, or pixel shader outputs back to the pipeline. This shader uses two semantics, each one is identified by the colon that precedes it. The first semantic identifies the input data to the function as position data from the vertex buffer. The second semantic identifies the function's return data as position data that will be output from the vertex shader. Semantics are covered in more detail in Chapter 7. The body of this shader is contained in one line:
return mul(inPos, WorldViewProj ); This vertex shader contains a single instruction, mul, which is an intrinsic function that performs a matric multiply. Intrinsic functions are native functions built into the language. There are many intrinsic functions listed in the HLSL Reference in Appendix C. In this example, mul takes two arguments: an input vector (inPos) and an input matrix (WorldViewProj). inPos is an argument of type float4 that contains four floating-point values. It is referred to as a four-component vector. If you remember the declaration of WorldViewProj, it contains 16 floating-point values, arranged like a 4-by-4 array. It's no coincidence that this matches the layout of a 4-by-4 matrix. The mul function multiplies the 1-by-4 vector and the 4-by-4 matrix, and yields a 1-by-4 vector. Another way to say the same thing is that mul transforms the position data by the world-view-projection matrix. The vertex shader operates once for each vertex in the object. The shader literally returns the transformed vertices. When the vertex shader completes, the object is ready to draw. (See Color Plate 13.) As you can see, the output is not very complex because the vertex data represents one triangle. It's a solid color, white, because we did not supply a vertex color, so the pipeline assumed the default value. The purpose of this tutorial is to demonstrate an HLSL vertex shader that performs a world-view-projection transform in one line of code. This tutorial calls D3DXCompileShader to compile the vertex shader. D3DXCompileShader is located in the RestoreDeviceObjects method, which is shown in the following code:
const char* strHLLVertexShader = D3DXCompileShader takes a shader in the form of a string. After the shader is compiled, call IDirect3DDevice9::CreateVertexShader to create the vertex shader object. The Glow example in Chapter 8 goes into more detail regarding the arguments used by these two API calls. Also, the section called "Building the Tutorials" later in this chapter goes into more detail about all of these API calls.
Add a Diffuse ColorTo expand the vertex shader to accommodate additional vertex data, we could add a diffuse color to each vertex of the triangle. The vertex shader would need to perform the position transformation (as earlier) and apply the per-vertex diffuse color. The shader could be modified to look like this:
float4x4 WorldViewProj; Now we have a polygon face with a little color in it. The top vertex was set to red, the lower-left vertex was set to white, and the lower-right vertex was set to blue. (See Color Plate 14.) Let's look behind the scenes to see what changes were needed. The first thing that's different with the shader is that it now returns a structure named VS_OUTPUT instead of a float4. As you can see, the structure contains two types of data: position and diffuse color.
struct VS_OUTPUT Both position and color data use a float4 type because each of them contains four components of data. The position contains (x,y,z,w) data, and the diffuse color contains (r,g,b,a) data. Both parameters have a semantic that identifies the original vertex data from the vertex buffer. Because the shader depends on the vertex data containing diffuse color (in addition to the position data), the vertex data needs to be modified to contain color data, as shown in the following code:
// Initialize three vertices for rendering a triangle This initialization consists of three rows of data, one for each vertex in the triangle. Each row (vertex) contains a position (x,y,z) and a diffuse color (rgba) created with the D3DCOLOR_RGBA macro. As a result of the vertex data changes, the vertex declaration also needs to be updated by adding another line, as shown here:
// Create the vertex declaration This declaration contains two lines, one for each type of data in the vertex buffer. The first row identifies the position data; the second corresponds to the diffuse color. That completes Tutorial 1, which gives you a starting place for generating HLSL shaders. These examples demonstrate a vertex shader that uses a single function with semantics. As you can see, HLSL uses functions to build shader functionality. The next tutorial will add a pixel shader to the mix so that you can see how a vertex shader talks to a pixel shader. So how do you get this shader to work on your machine? As you might know from looking at the DirectX SDK samples, all the samples run on the sample framework. This approach is continued here, for all the examples because the sample framework provides so much base Microsoft Windows functionality and allows you to focus on graphics issues. To learn more about the API calls used by the runtime to get Tutorial 1 up and running, see "Building the Tutorials" later in this chapter. For details about vertex declarations, see the DirectX 9 SDK documentation.
Tutorial 2: Add a Pixel ShaderNow that we've seen some of the basics of an HLSL vertex shader, let's add a pixel shader. In this example, we'll continue to transform the vertices with a vertex shader, but now we'll apply a texture with a pixel shader. Here are the new shaders:
float4x4 WorldViewProj; Now there are two shaders to compile: the vertex shader for transforming position and the pixel shader for sampling a texture. The vertex shader also demonstrates the in and out keywords available in HLSL. These keywords identify shader function arguments as inputs only (in), outputs only (out), or both (inout). In this example, the vertex shader returns two values. Instead of returning them as member of a structure (as in Tutorial 1), the out keyword is applied to them in the function's argument list. Notice also that the texture coordinate semantic TEXCOORD0 is the same for the vertex shader input, vertex shader output, and pixel shader input. That's because this data is first read from the vertex buffer (the semantic on the in parameter), then written out by the vertex shader (the semantic on the out parameter), and then read by the pixel shader. This is an example of using a semantic to associate data from vertex shader outputs to pixel shader inputs. The vertex shader transforms the position to projection space and passes the texture coordinates out. The pixel shader uses the texture coordinates to sample a texture and outputs a per-pixel color. Here's the code to create both shaders:
// Compile and create vertex shader Tutorial 1 uses D3DXCompileShader, which takes a shader in the form of a string contained in the project source code. This tutorial uses D3DXCompileShaderFromResource, which loads and compiles the shader file as a resource. A resource specifies the name of the shader file, which is compiled into the executable. The result is that the compiled shader ends up in the executable file (.exe). In this example, the resource string, ID_EXAMPLE1_FX, is loaded with the shader file name using the Microsoft Visual Studio resource editor. Each time the shader is modified, the project must be rebuilt so that the resource is updated in the executable file. Because the pixel shader will use a texture, a texture object needs to be loaded. Here's the code to create the texture object:
TCHAR szEarth[MAX_PATH]; Here's the vertex declaration:
// Create the vertex declaration The resulting object is a sphere with a texture map of the earth applied. (See Color Plate 15.) Now that we have the pixel shader working, let's experiment with a few image options.
ComplementingFirst let's complement the texture color data, which should invert all the colors.
void PS_HLL_EX1( Each of the color components (rgba) is at full intensity when it is equal to 1. Therefore, to complement the color, take each color channel and subtract it from 1. Reds now appear as cyan, greens now appear magenta, and blues appear yellow, which explains why the blue oceans look mostly yellow. (See Color Plate 16.) Notice how HLSL is performing vector math. oCol is a four-component vector declared as a float4 type. The tex2D intrinsic function samples the texture using the coordinates in vTex and returns a four-component color. Think of this four-component vector as an rgba color. The expression 1.0f - tex2D(...) performs a component-wise subtraction, which conceptually looks like this:
oCol.r = 1.0 - red value;
DarkeningWe can just as easily darken the image. Because 1.0 is a full-intensity value, reducing all components by the same amount results in darker color components. In this case, the image is darkened by reducing the color values by a factor of 2.
void PS_HLL_EX1( Notice how the oceans are still blue and the continents are still predominantly green and yellow. The image is simply using darker colors. (See Color Plate 17.) Just like the complement example, this example uses vector math equivalent to the following:
oCol.r = 0.5f * red value;
Masking the Red OutThe complement and darken examples use vector math to change all the color components by the same arithmetic equation. This example isolates the red component and filters it out.
void PS_HLL_EX1( This pixel shader samples the texture just as the first example did. The red component is effectively filtered out by setting it to 0, as shown here:
oCol.r = 0.0f; The lack of red is not particularly obvious in the oceans, but notice how the mountain ranges in South America are almost green because the red component in them has been removed. (See Color Plate 18.)
Displaying Red OnlyIs it hard to see how much red was in the mountains? Let's simply modify the pixel shader to display the red component. In other words, let's mask out the green, blue, and alpha components.
void PS_HLL_EX1( This image is only non-black where the red component is non-zero. There are no blue, green, and alpha components in this image, which is why the oceans are almost black. (See Color Plate 19.) As you can see, there are a number of interesting effects that can be created with very small modifications in the pixel shader instructions.
Tutorial 3: Add a Procedural TextureThis tutorial demonstrates how to generate a procedural texture, which is used to texture an object. A procedural texture is a texture that's generated with mathematical equations. Procedural textures are one way to use noise functions to add realism to a textured object.This tutorial procedurally generates a grid texture containing horizontal and vertical lines. (See Color Plate 20.) A procedural texture is generated at run time. It's usually called in a one-time startup function such as InitDeviceObjects to fill an existing texture object. Once the texture is loaded, the texture object can be accessed by the multitexture blender or it could be used in a pixel shader. A procedural texture is treated like a third type of shader, in the sense that the same sequence of APIs are used to create it. Call D3DXCompileShader with a special target, tx_1_0, to indicate that a procedural texture is being created. First create a texture object. The texture object from Tutorial 2 is created from a texture file. Because we'll be creating our own texture, we could do this instead:
// Create the procedural texture This code generates a texture object that's 64-by-64 with one level and default settings for the format and the memory pool. The texture object will be loaded when the procedural shader runs. So let's see how to generate the procedural shader before we use it. Here's the function that will generate the procedural shader:
void TX_HLL_EX1( The function is called TX_HLL_EX1. It essentially draws horizontal and vertical lines at certain texture coordinates using a series of if statements. This is a simple texture that will illustrate the API calls necessary to use a procedural texture. The procedural texture (the shader) is compiled by calling D3DXCompileShaderFromResource.
// Create the procedural texture The entry point, TX_HLL_EX1, identifies the shader function that will be called to procedurally create the texture. The texture shader version tx_1_0 identifies the shader as a version 1_0 texture shader. The shader function will be called by D3DXFillTextureTX, as shown below:
// Procedurally fill texture So there you are. Now we have a vertex shader, a pixel shader, and a texture shader. The texture shader is compiled just like a vertex or pixel shader. It requires a texture object to be created so that it can procedurally fill it. Instead of running at draw time like a vertex or pixel shader, the texture shader runs when D3DXFillTextureTX is called.
Building the TutorialsA few setup steps are necessary to build these shaders. All the examples in this book are built using the sample framework, just like all the rest of the samples in the DirectX 9 SDK. The sample framework is a set of classes that perform much of the basic Windows housekeeping for managing the objects in a DirectX application. Table 6-1 shows the methods that the sample framework provides for adding our code.Table 6-1 Sample Framework Methods
Of these methods, the most interesting work involves creating the resources the program will use and rendering the output. Therefore, we'll focus most of this section on the RestoreDeviceObjects and Render methods.
Creating ResourcesFor these tutorials, we need to create an object for each scene. We might need to create a vertex shader to transform and light the object per-vertex. We might need a pixel shader to texture and light the object per-pixel. We might need a texture shader to generate a procedural texture. We might need to create a texture or two, set up one or more samplers, and set up the multitexture blender to blend the results. Each of these objects is referred to as a resource. All these resources are typically created in RestoreDeviceObjects because this method is called whenever the device is lost and the application needs to re-create resources.Here is the CMyD3DApplication::RestoreDeviceObjects method from Tutorial 1:
HRESULT CMyD3DApplication::RestoreDeviceObjects() In this example, RestoreDeviceObjects creates the shaders, initializes the vertex buffer with the vertex data, creates a vertex declaration to describe the vertex buffer, calls RestoreDeviceObjects to generate font resources, sets a few default render states, and initializes two of the three matrices. If you want more detail, keep reading. If you feel comfortable with resource creation, you can skip ahead a few pages to the drawing code or even to the next chapter. Creating a Shader A shader is a series of HLSL statements that need to be validated and compiled before they can be used by the runtime. Here's one way to accomplish this:
const char* strHLLVertexShader = In this example, the shader is a text string supplied as an argument to D3DXCompileShader. D3DXCompileShader has two other variationsD3DXCompileShaderFromFile and D3DXCompileShaderFromResourceso you have a variety of formats for supplying shader code. D3DXCompileShader takes several arguments and can return several pointers. This example specifies the shader in a string with the shader entry point function VertexShader_Tutorial_1, and with shader version vs_1_1. If the function is successful, it returns a pointer to the compiled shader code in pShader and a pointer to the constant table, m_pConstantTable. The constant table pointer will be used to initialize the shader global variables. Use the pointer to the compiled shader to create the shader object by calling IDirect3DDevice9::CreateVertexShader. That's it. We now have a compiled vertex shader. Tutorial 2 showed how to create a pixel shader, and Tutorial 3 showed how to create a texture shader. Creating the Vertex Data This example declares the vertex data in the vertices array so that it's easy to see. Each line in the array is a single (x,y,z) vertex position.
// Initialize three vertices for rendering a triangle With the vertex data defined, we need to load the data into the vertex buffer. Here's the sequence for creating and loading the vertex buffer:
// Create the vertex buffer. Here we are allocating enough memory We've created a vertex buffer with room for three values per vertex (sized by CUSTOMVERTEX), a default usage, a 0 FVF (because the vertex declaration will describe the vertex buffer data), and a default memory pool so that the runtime can decide what type of memory is most efficient for storing the data. Once the buffer is created, the Lock/Unlock sequence is used to fill the buffer. The vertex declaration describes the data in the vertex buffer. This example contains one line because the vertex buffer contains only one data type: position data.
// Create the vertex declaration This example specifies data in stream 0, no offset from the stream pointer to the data, three floating-point values (position data), a default method (requiring no special tessellator processing), and a default usage index (0). In other words, the vertex buffer contains only (x,y,z) position data that will not need to be tessellated. Initializing the Render States Render states set up the pipeline to process vertex and pixel data. Try to set render states as infrequently as possible to improve efficiency. In this section, we're explicitly turning off the lighting engine and setting the cull mode to tell the pipeline to draw all polygon faces.
// Set up render states This is also a good place to do one-time initializations, such as setting matrices. In this case, the world and projection matrices are set because this application does not expect them to change. (The view matrix is set up in OneTimeSceneInit.)
RenderingThe render code takes advantage of all the resources we created, sets shader variables, and calls the draw method.
HRESULT CMyD3DApplication::Render() The render code initializes the shader matrix (WorldViewProj), sets the vertex declaration (which describes the vertex buffer), sets the vertex shader, sets the stream source, and calls DrawPrimitive to draw the triangle. The vertex shader is set back to NULL after the draw call. This is similar to resetting render states or texture stage states after rendering.
SummaryThis chapter illustrated three shader tutorials. Tutorial 1 rendered a single triangle with a vertex shader. Tutorial 2 showed how to use the vertex shader outputs as the pixel shader inputs. By changing only the pixel shader, we created a variety of simple image-processing results. Tutorial 3 showed how to create and fill a procedural texture using a texture shader. With HLSL, it's easy to create shaders in a C-like language. If you're ready for more, the next chapter shows you how to create expressions, statements, and functions to build your own shaders.
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||