11 min read

Ambient Lighting

Introduction

Well, now that we’ve discussed the basics of shaders and the programmable graphics pipeline, we are ready to move on and make our first shader! In this tutorial, we will write an extremely simple shader that performs ambient lighting. Remember from earlier in the 3D graphics tutorials, that ambient lighting is just simply uniform light that sort of comes from the scene itself, rather than a specific light source. In reality, ambient light is just light that originally came from a source but has bounced off of objects in the scene.

In this tutorial, we will first start by taking a look at how shaders are written in HLSL, using effect files. Effect files have the extension .fx. We will look at the basic things that need to go into an effect file, and finally, we will write our code for ambient lighting. In the next tutorial, we will use our shaders in an XNA game, and then after that, we will continue on to some more advanced shader effects.

The completed code for the effect file can be found near the bottom of the page, so if you get lost, feel free to jump down there to see how everything will fit together.

.FX Files

It appears that there are a few different ways that people can write shaders. In XNA, the most commonly performed method is by using effect files. These files usually have the extension .fx. They are written in HLSL and usually contain four parts. The first part is variables. These variables can be set from inside an XNA game. The second part is a definition of the data structures that will be used by your shaders. Typically, at least two data structures are created–one that represents vertex data as it is coming into the vertex shader, and one that represents the data between the vertex shader and the pixel shader. Additionally, I have seen a few shaders that define an output from the pixel shader, but we won’t worry too much about that, since it is simply the color of the pixel to be drawn, and we can just return a color value instead. The third thing that an effect file will contain is a definition and implementation of the shaders we will use. Usually, we will have at least one vertex shader and one pixel shader, but you can put multiple shaders in a single file, and in some cases, you don’t even need a vertex shader. The fourth component that is in an effect file is a definition of the “techniques” that we want to use. A technique is a way of telling the graphics card when to use the various shaders that we have created. A technique is composed of a number of passes, which each have its own vertex and pixel shader. Each pass is performed in sequence and the results are combined. For now, we will stick with a single pass, but don’t forget in the future that you can have multiple passes.

The Ambient Light HLSL Code

We are now ready to begin working on our first shader.] This shader is going to be about as simple as it gets, but hopefully, it will help you understand the basics of creating effect files. We will create an effect that performs ambient lighting.

Creating a new .FX File

Our first step is to create a new file to put our effect in. In the Solution Explorer, go to your game’s Content project. We’ve added a lot of different types of content in the past, and effect files are no different. I usually add an Effects directory to my project where I put all of my effect files, but this isn’t necessary. Locate the directory inside of the Content node where you want to place your new effect file and right-click on it. In the popup menu that appears, choose Add > New Item. The New Item dialog will appear. Choose the Effect File template, and give your file a name. I’ve called mine “Ambient.fx”, as shown in the image below:

Screenshot 1

This will create a new file with some text in it. This code is actually fairly similar to what we want in the end, but for now delete everything in the file. We will write everything from scratch. When you are done with this tutorial, you can go back and compare the two.

Adding Variables

In C#, you can place instance variables anywhere you want to in the class. In HLSL, you have to declare variables before you use them in the file. So typically the first thing you see in a file is a listing of all of the variables that we will use. Declaring a variable in HLSL is similar to declaring a variable in C#. You indicate the type of the variable, followed by a name for the variable, and then optionally, a default value.

For now, we are going to create five variables. So in your file (which should now be completely empty), add the following five lines of HLSL code:

float4x4 World;
float4x4 View;
float4x4 Projection;

float4 AmbientColor = float4(1, 1, 1, 1);
float AmbientIntensity = 0.1;

The first three are of type float4x4. This is basically a 4-by-4 matrix, similar to the ones that we’ve been using inside of XNA. These three variables will directly correspond to the world, view, and projection matrices in our game. It is probably worth mentioning that many times, people will combine these three matrices into one single matrix (usually called something like wvpMatrix). This method is probably slightly more efficient (if it is done right), but also less clear. I am going to keep using three separate matrices while we are learning, but once you feel like you’ve got the hang of HLSL, feel free to switch over to the single matrix, and combine the three inside of your XNA game.

The last two variables are used for ambient lighting. The first one, AmbientColor is of type float4, which is essentially an array with four elements in it. This will be the color of our ambient lighting, and we give it a default value of white. The last variable, AmbientIntensity is of type float, which is identical to the float type in C#. We have our intensity set to only a small amount for now. Feel free to come back and play around with these values after you have gotten the effect working correctly.

Defining the Data Structures

The next step is for us to create the necessary data structures to store data between three places: between the application and the vertex shader (this is the input to the vertex shader), between the vertex shader and the pixel shader (this is the output of the vertex shader, and also the input to the pixel shader), and for after the pixel shader, though this is usually just a color, and we will handle it in a slightly different way.

We will first create the structure for the input to the vertex shader (which comes from the application). Add the following code to your file:

struct VertexShaderInput
{
    float4 Position : POSITION0;
};

This structure is pretty simple. The name of the structure, VertexShaderInput can be anything you want it to be since it is similar to a class name in C#. I have called mine VertexShaderInput because it is the input to the vertex shader, but it is also often called something like ApplicationToVertex because this is the type of data structure that information is contained in while it is transferred from your application to the vertex shader.

This data structure has one variable in it called Position (once again, this can be anything), which is of type float4, which is essentially a vector. Notice the : POSITION0 after the variable name. This is what is called a semantic . In this case, it is an “input semantic”. All variables that are used to store data before, in between, or after the shaders must be identified with a semantic. We will discuss a few more as we go along, but a list of them can be found at the [http://msdn.microsoft.com/en-us/library/bb509647(VS.85).aspx MSDN Library].

Next, we will create the structure for the output from the vertex shader, which is also the input to the pixel shader. Add the following code to your file:

struct VertexShaderOutput
{
    float4 Position : POSITION0;
};

This structure is actually identical to the vertex shader input for now, but in other shaders that we make in the future, this won’t be the case. Once again, this can be called anything that you would like. Because this is how things are stored between the vertex shader and the pixel shader, it is also sometimes called something like VertexToPixel.

The Vertex Shader

The next step is to write the vertex shader itself. Remember, the vertex shader has the job of transforming vertices and doing the necessary lighting calculations so that they can be colored later in the pixel shader. For the case of ambient lighting, all we need to do is to perform our vertex transformations, so the vertex shader will be quite simple. Add the following code to your HLSL file now:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    return output;
}

This is a function, fairly similar to functions (methods) in C#. We first indicate the return type of the function, which in this case is our VertexShaderOutput structure. We give the function a name, which, once again, can be anything you want. I see a lot around that are simply called VS. Then in parentheses, you give the input type to the function, which is an instance of our VertexShaderInput structures that is called input.

Inside of the function, on the first line, we create a VertexShaderOutput field, which will eventually store the end results of our vertex shader processing. Next, we do the calculations we need and set the appropriate fields in the output. In this case, we take the position of the input vertex and transform it by the world matrix, then the view matrix, then the projection matrix, which puts it in the correct location. Notice that we use the mul function, which is a multiplication function. This is how the transformation is performed on the position vector, just like in XNA. There are a lot of other functions that are available in HLSL, many of which we will get around to later on. A complete list can be found at http://msdn.microsoft.com/en-us/library/bb509611(VS.85).aspx.

Creating the Pixel Shader

The next step we will do is to create the pixel shader. Because we are only doing ambient lighting, the pixel shader is fairly simple. Add the code below to your HLSL file:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return AmbientColor * AmbientIntensity;
}

This is similar to the vertex shader. Once again, we specify the return type of the function, which in our case is going to be a color, represented by a float4 (an array of floats). We give it a name and define the input to the function. Notice that there is a : COLOR at the end of the function. The is a semantic for the data that is returned from the function. We essentially tell the graphics card that this function is returning the color. Some people prefer to create an entire data structure for the output from the pixel shader in a similar manner to our VertexShaderInput and VertexShaderInput. In that case, the COLOR0 semantic would be placed with the color variable of that structure, and would not be needed here.

The math for ambient lighting is pretty simple. Every pixel in the triangle will have the same value, which is the color of the ambient light multiplied by the intensity of the ambient light.

Defining the Technique

The last thing we need to do is define a technique so that it can be used by our XNA game. Add the following code to the bottom of your effect file:

technique Ambient
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

We give our technique a name that can be referred to later. I’ve called mine “Ambient”. Then inside, we list the passes that are in the technique. For now, we only need one pass. In each pass, we define the vertex shader and pixel shader that we will use. (In DirectX 10, you can also optionally define a geometry shader to use.) You tell it which vertex shader to use with the statement compile vs_2_0 VertexShaderFunction(). We put the keyword compile because we need it to compile our shader into something that can be executed on the graphics card. Next, we state the version of the shader that we want to use. Here, we are using vertex shader 2.0. There are several other versions. Higher versions have more capabilities, which allow you to do more things, but higher versions aren’t supported by all graphics cards. You should always use the lowest version that supports everything you need. (Though XNA 4.0 no longer supports pixel shader versions before 2.0.)

Additionally, you may need to create multiple effects that use different versions, so that people with low-end graphics cards can play your game, but also, people with high-end graphics cards can get all of the cool features that you want it to have. Then you can choose the correct effect in your game, based on the capabilities of the graphics card. Of course, the last step in the line simply states which function to use for the vertex shader. The pixel shader is done in a similar manner.

That completes the code for our shader and we are now ready to move on and apply our shader in an XNA game. The completed code for the shader is below:

float4x4 World;
float4x4 View;
float4x4 Projection;

float4 AmbientColor = float4(1, 1, 1, 1);
float AmbientIntensity = 0.1;

struct VertexShaderInput
{
    float4 Position : POSITION0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return AmbientColor * AmbientIntensity;
}

technique Ambient
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}