Wednesday, December 19, 2007

Lighting Demo

Creating a Simple Shader

First, before anything, we need to create the shader that will be used to render our pixels onto the screen. For starters, all the shader will do is transform the geometry into correct world, view, and projection space, and then output all pixels white. Later, however, we will add cool lighting effects for all kinds of materials.

Open up an empty Notepad document. Click the File menu tab and then click Save As. Name the document Transform.fx (you will need to switch the Files of Type box to All Files). Finally, place the document somewhere easily accessed and click Save.

Effect Parameters

The first things we want to add to our shader are effect parameters. These types of variables are used to allow access for the XNA app so that we can manipulate how the shader renders the geometry. At the moment, we only need one parameter that holds the world, view, and projection matrices. We need this parameter to transform all the geometry (vertices) from their local space coordinates to the correct world, view, and projection space.

At the top of the file, add this code:

uniform extern float4x xWorldViewProjection;

uniform extern just means that this variable is like a parameter that can be accessed by the XNA app. float4x4 is a type that defines a 4x4 matrix variable (which means the same as Matrix in XNA).

The VertexInput Structure

The next step we need to take is to create a structure that defines what type of data the vertex shader takes in from the XNA app.

All the data that we want to take in for now is vertex position data (we do not need color data because we will just output all pixels white). With this in mind, we will have a very simple VertexInput structure. Underneath the effect parameter, create the VertexInput:

// Vertex input
struct VertexInput
{
float3 pos : POSITION;
};

As you may have guessed, float3 is a keyword that creates a variable similar to an XNA Vector3. Also, we have the : POSITION0. This is called a variable semantic. The reason for using these is so that the shader knows how to map data incoming from the XNA app to their corresponding variables in the shader (we don’t want the shader to confuse vertex normal data with vertex position data).

The VertexOutput Structure

After the VertexInput structure, we create the VertexOutput. The VertexOutput structure defines what type of data will be given to the pixel shader. The pixel shader is called after the vertex shader, but does processing on every pixel instead of every vertex. Some operations only need to be done per-vertex instead of per-pixel which can increase performance (generally there are many more pixels than vertices). These operations are completed in the vertex shader. When the pixel shader comes into play, it takes in data from the vertex shader to use for processing the pixels. We declare this data structure in a VertexOutput structure.

Below the VertexInput structure, we can now create the VertexOutput:

// Vertex output
struct VertexOutput
{
float4 pos : POSITION;
};

All we need to output to the pixel shader is the position of the vertex once it has been transformed into correct space. The position has also now been changed to a float4, which could be looked at like an XNA Vector4. The reason for this is so that we can multiply this position with matrices. Also, you may be wondering why we don’t output any color. Usually, we would output a vertex color, but, because our simple pixel shader will only output white color data for every pixel, it would be a waste to store color output for each vertex.

The Vertex Shader

Now we can create the vertex shader. The vertex shader is called first and does operations on the vertices, also known as the geometry, of the mesh to be drawn.

Below the VertexOutput structure, add the vertex shader:

// Vertex shader
VertexOutput VS_Transform(VertexInput input)
{
// Zero out our output.
VertexOutput output = (VertexOutput)0;

// Transform the position of the vertex into correct world,
// view, and projection space.
output.pos = mul(float4(input.Pos, 1.0f), xWorldViewProjection);

// Done--return the output.
return out;
}

This is a very, very simple vertex shader. The first line in the vertex shader zeroes out the output that the vertex shader will return. The next line transforms the vertex into correct world, view, and projection space. We use the HLSL intrinsic mul function to multiply a float4 and a float4x4. However, the in-position of the vertex is a float3, which means we need to convert the float3 to a float4. We do this by creating a float4 and passing the in-position and 1 as parameters to the constructor (we pass in 1 as the w-component because the position is a location vector (point). If the vector were a normal or other vector that did not have a position on the screen, but only a direction, then we would specify 0 for the w-component). Also, you will notice that we multiply the position, now in homogeneous (4-component) coordinates, by the world, view, and projection matrices. These matrices can change for each object in the application, thus explains why we make this variable an effect parameter.

Finally, we are finished manipulating the output and can give it to the pixel shader. Note though, that the pixel shader will not actually use the output’s position. Even in advanced shaders, the pixel shader uses color data and texture data. The output’s position is used by the graphics card to place the vertices in correct world, view, and projection space.

The Pixel Shader

The pixel shader is called after the vertex shader and does processing on all of the pixels in the geometry, finally outputting a color for the pixel. Below the vertex shader, create the pixel shader:

// Pixel shader
float4 PS_Transform() : COLOR
{
// Output white for each pixel.
return float4(1.0f, 1.0f, 1.0f, 1.0f);
}

Usually, lighting calculations and other processing for cool effects would be done here, but for now, all we do is output the color white. Notice, however, that we didn’t take in a VertexOutput parameter to the pixel shader. Usually we would but because we don’t need any of the information in the VertexOutput structure, we don’t need to bother with parameters. Finally, a unique detail of the pixel shader is that it represents a color. The pixel shader has a return value of a float4, which is like a color (r, g, b, a) and also has a : COLOR semantic attached to it. Also, note that float4, which can be interpreted as an XNA Vector4, can also represent an XNA Color, which has four components as well.

The Technique

We are now 90 percent done with our simple shader. All we have left to do is add the technique, which respectively calls the vertex and pixels shaders associated with that technique. It is possible to have many different techniques, inputs, outputs, vertex shaders, pixel shaders, and effect parameters in one shader file. You could use this ability to call different vertex and pixel shaders depending on what kind of shader model the user’s computer could support. In the XNA app you would determine which level the computer can support and then call the corresponding technique (e.g. WaterTech1, WaterTech2) which would call the correct vertex and pixel shaders for your computer. This allows for extra flexibility because the user could use the same application without having to get a better graphics card. For us, however, we will just use shader model 2.0.

Below everything you have so far, create the technique for our transform shader:

// Transform
technique Transform
{
pass P0
{
// Compile the vertex and pixel shaders for this
// shader using vertex and pixel versions 2.0.
vertexShader = compile vs_2_0 VS_Transform();
pixelShader = compile ps_2_0 PS_Transform();
}
}

First, we declare the technique and the name of the technique. Next, we declare a pass. A pass is a loop inside the technique that calls the associated vertex and pixel shaders. A technique may want to contain more than one pass for special effects so in the XNA app you need to loop through the passes in the currently set technique. Note however that using two passes doubles the processing time for the geometry, and using three passes makes it even worse. If you don’t need to use multiple passes, it would probably be best not to.

Inside the pass, we set the vertex and pixel shaders, respectively, to the vertex and pixel shaders we made previously. Also, we compile our vertex and pixel shaders using vertex and pixel versions 2.0.

We are now done creating the shader, which can be used in any application. We learned how to create effect parameters, vertex input and output structures, vertex shaders, pixel shaders, and techniques. Also, hopefully you learned a bit about the structure of HLSL and how to do some basic coding with it. In the next parts, we will learn how to apply shaders to geometry in the XNA application.

1 comment:

Zygote said...

Great work! I'd like to see the next article where you go into the implementation as well as the sample download :)

Thanks,
Ziggy
Ziggyware XNA News and Tutorials