Ambient light

Hi, and welcome to tutorial 1 of my XNA Shader Tutorial series. My name is Petri Wilhelmsen and is a member of Dark Codex Studios. We usually participate in various competitions regarding graphics/game development, at The Gathering, Assembly, Solskogen, Dream-Build-Play, NGA and so on.

The XNA Shader Programming series will cover many different aspects of XNA, and how to write HLSL shaders using XNA and your GPU. I will start with some basic theory, and then move over to a more practical approach to shader programming.The theory part will not be very detailed, but should be enough for you to get started with Shaders and be able to experiment for yourself. It will cover the basics around HLSL, how the HLSL language works and some keywords that is worth knowing about.

Today I will cover XNA and HLSL, as well as a simple ambient lighting algorithm.

I Prerequisites

Some programming in XNA, as I wont go much into details about loading textures, 3d models, matrices and some math.

II A short history about shaders

Before DirectX8, GPU's had a fixed way to transform pixels and vertices, called "The fixed pipeline". This made it impossible to developers to change how pixels and vertices was transformed and processed after passing them to the GPU, and made games look quite similar graphics wise. DirectX8 introduced the vertex and pixel shaders, that were a method developers could use to decide how the vertices and pixles should be processed when going through the pipeline, giving them a lot of flexibility.An assembly language was used to program the shaders, something that made it pretty hard to be a shader developers, and shader model 1.0 was the only supported version. But this changed once DirectX9 was released, giving developers the opportunity to develop shaders in a high level language, called High Level Shading Language( HLSL ), replacing the assmely shading language with something that looked more like the C-language. This made shaders much easier to write, read and learn.DirectX10.0 introduced a new shader, the Geometry Shader, and was a part of Shader Model 4.0. But this required a new state-of-the-art graphics card, and Windows Vista.XNA supports Shader Model 1.0 to 3.0, but works on XP, Vista and XBox360!

III Shaders?

Well, enough history.. Really, what is a shader?

As I said, shaders can be used to customize steps in the pipeline to make it up to the developer to implement how pixels/vertices should be processed.

As we can see from the figure below, an application got initiates and uses a shader when rendering, the vertex buffer works with the pixelshader by sending required data from the vertex shader to the pixel shader, working together to create an image to the framebuffer.

One important fact to note behind your ears is that many GPUs does not support all shader models. This should be accounted for when developing shaders. One shader should have alternate methods to archive similar/simpler effects, making the application work on older computers.

IV Vertex Shaders

Vertex shaders is used to manipulate vertex-data, per vertex. This can for example be a shader that makes a model “fatter” during rendering by moving vertexes along their normals to a new position for every vertex in the model (deform shaders).

Vertex shaders got input from a vertex structure defines in the application code, and loads this from the vertex buffer, passed into the shader. This describes what properties each vertex will have during shading: Position, Color, Normal, Tangent++.

The vertex shader sends its output to for later use in the pixel shader. Do define what data the vertex shader will pass to the next stage can be done by defining a struct in the shader, containing the data you want to store, and make the vertex shader return this instance, or by defining parameters in the shader, using the out keyword. Output can be Position, Fog, Color, Texture coordinates, Tangets, Light position and so on.

V Pixel Shaders

The Pixel shader manipulates all pixels( per pixel ) on a given model/object/collection of vertices. This can be a metal box, where we want to customize the lighting algorithm on, colors and so on. The pixel shader gets data from the vertex shaders output values, like position, normals and texture coordinates:

VI HLSL

High Level Shading Language is used to develop shaders. In HLSL, you can declare variables, functions, datatypes, testing( if/else/for/do/while+) and much more, in order to create a logic for vertices and pixels. Below is a table of some keywords that exists in HLSL. This is not all of them, but some of the most important ones.

Matrices in HLSLfloat3x3: a 3x3 matrix, type floatfloat2x2: a 2x2 matrix, type float We also have a lot of helper functions in HLSL, which help us archive complex mathematical expressions.cos(x) Returns cosine of xsin(x) Returns sinus of xcross(a, b) Returns the cross product of two vectors a and b dot(a, b) Returns the dot product of two vectors a and bnormalize(v) Returns a normalized vector v ( v / |v| )

HLSL offers a huge set of functions just waiting for you to use! Learn them, so you know how to solve different problems.

VII Effect files

Effect files (.fx) makes shader developing in HLSL easier, and you can store almost everything regarding shaders in a .fx file. This includes global variables, functions, structures, vertex shader, pixel shader, different techniques/passes, textures and so on.

We have already seen how to declare variables and structures in a shader, but what is this technique/passes thing? It’s pretty simple. One Shader can have one or more techniques. Each technique can have a unique name and from the game/application, we can select what technique in the shader we want to used, by setting the CurrentTechnique property of the Effect class.

effect.CurrentTechnique = effect.Techniques["AmbientLight"];

Here, we set “effect” to use the technique “AmbientLight”. One technique can have one or more passes, and we must remember to process all passes in order to archive the result we want.

This tells us that the technique will use VS2() as the vertex shader, PS2 as the pixel shader, and will support shader model 1.1 or higher. This makes it possible to have a different and more complex shader for GPUs supporting higher shader model versions.

VIII Implementing the shaders in XNA

Its really easy to implement shaders in XNA. In fact, only a few lines of code is needed to load and use a shader. Here is a list of steps that can be followed when making a shader:

where "matWorldViewProj" is defined in the shader: float4x4 matWorldViewProj; and worldMatrix * viewMatrix * projMatrix is a matrix that matWorldViewProj is set to.SetValue sets a value to the parameter and sends it to the shader, and GetValue<Type> retrives a value from the shader, where Type is the datatype to retrive. For example, GetValueInt32() gets an integer from the shader.

8. Render the scene/object you want this shader to process/transform.

9. To stop the pass, call pass.End() and to stop the shader, call the End() method of Effect:

pass.End();
effect.end();

To understand this better, open the source code provided and see the steps in action.

IX Ambient light

Ok, we are finally at the last step, implementing the shader! Not bad eh?First of all, what is an "Ambient light"?Ambient light is the basic light in a scene that’s just there. If you go into a complete dark room, the ambient light is typically zero, but when walking outside there is almost always some light that makes it possible to see. This light got no direction and is here to make sure objects that are not lit, will have a basic color.

The formula for Ambient light is:I = Aintensity x Acolor (1.1)

where L is the light, Aintensity is the intensity of the light( usually between 0.0 and 1.0, and Acolor is the color of the ambient light. This color can be a hardcoded value, a parameter or a texture.

Ok, let's start implementing the shader. First of all, we need a matrix that represents the world matrix:

float4x4 matWorldViewProj;

Declare this in the top of the shader as a global variable.Then, we need to know what values the vertex shader will pass to the pixel shader. This is done by creating a structure( you can name it to whatever you want):

struct OUT
{
float4 Pos: POSITION;
};

We create a structure named OUT that contains a variable of the type float4 with the name Pos. The POSITION in the end tells the GPU what register to put this value in. So, what is a register? Well, a register is simply just a container in the GPU that contains data. The GPU got different registers to put position data, normal, texture coordinates and so on, and when defining a variable that the shader will pass to the pixel shader, we must also decide where in the GPU this value is stored.

We create the vertex shader function of the OUT type, where it takes in the parameter float4 Pos: POSITION. This is the position of the vertex defined in the model file/application/game.

Then, we make an instance of the OUT structure name Out. This structure must be filled and returned from the function for later processing.The position we have in the input parameter is not processed, and needs to be multiplied with the worldviewprojection matrix in order to be placed correctly on the screen.As this is the only variable in OUT, we are ready to return it and move on.Now, its the pixel shaders turn to make a move. We declare this as a float4 function, returning a float4 value stored in the COLOR register of the GPU.It’s in the pixel shader we will compute the ambient light algorithm: