Ambient light
Adapted to XNA 4.0 by A. Nadif. To see the original article for XNA 3.1, follow this link.
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.
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!
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.
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.
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.
struct VS_OUTPUT { float4 Pos: POSITION; }; VS_OUTPUT VS( float4 Pos: POSITION ) { VS_OUTPUT Out = (VS_OUTPUT) 0; ... return Out; } // or float3 VS(out float2 tex : TEXCOORD0) : POSITION { tex = float2(1.0, 1.0); return float3(0.0, 1.0, 0.0); }
You should never call your Vertex and Pixel Shader "VertexShader" and "PixelShader". Using such names would trigger errors.
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:
float4 PS(float vPos : VPOS, float2 tex : TEXCOORD0) : COLOR { ... return float4(1.0f, 0.3f, 0.7f, 1.0f); }
The pixel shader can have two output values, Color and Depth.
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.
Examples of datatypes in HLSL
bool true or false
int 32-bit integer
half 16bit integer
float 32bit float
double 64bit double
Examples of vectors in HLSL
float3 vectorTest
float vectorTest[3]
vector vectorTest
float2 vectorTest
bool3 vectorTest
Matrices in HLSL
float3x3: a 3x3 matrix, type float
float2x2: 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 x
sin(x) Returns sinus of x
cross(a, b) Returns the cross product of two vectors a and b
dot(a, b) Returns the dot product of two vectors a and b
normalize(v) Returns a normalized vector v ( v / |v| )
For a complete list: http://msdn2.microsoft.com/en-us/library/bb509611.aspx
HLSL offers a huge set of functions just waiting for you to use! Learn them, so you know how to solve different problems.
Examples of datatypes in HLSL
bool true or false
int 32-bit integer
half 16bit integer
float 32bit float
double 64bit double
Examples of vectors in HLSL
float3 vectorTest
float vectorTest[3]
vector vectorTest
float2 vectorTest
bool3 vectorTest
Matrices in HLSL
float3x3: a 3x3 matrix, type float
float2x2: 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 x
sin(x) Returns sinus of x
cross(a, b) Returns the cross product of two vectors a and b
dot(a, b) Returns the dot product of two vectors a and b
normalize(v) Returns a normalized vector v ( v / |v| )
For a complete list: http://msdn2.microsoft.com/en-us/library/bb509611.aspx
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.
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 is an example of a shader containing one technique and one pass:
This is an example of a shader containing one technique and one pass:
technique Shader { pass P0 { VertexShader = compile vs_2_0 VS(); PixelShader = compile ps_2_0 PS(); } }
This is an example of a shader containing one technique and two passes:
technique Shader { pass P0 { VertexShader = compile vs_2_0 VS(); PixelShader = compile ps_2_0 PS(); } pass P1 { VertexShader = compile vs_2_0 VS_Other(); PixelShader = compile ps_2_0 PS_Other(); } }
This is an example of a shader containing two techniques and one pass:
technique Shader_11 { pass P0 { VertexShader = compile vs_2_0 VS(); PixelShader = compile ps_2_0 PS(); } } technique Shader_2a { pass P0 { VertexShader = compile vs_2_0 VS2(); PixelShader = compile ps_2_a PS2(); } }
We can see that a technique got two functions, one for the pixel shader and one for the vertex shader.
VertexShader = compile vs_2_0 VS2(); PixelShader = compile ps_2_0 PS2();
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.
Note that low level shader APIs are no longer supported in XNA 4.0
Note that low level shader APIs are no longer supported in XNA 4.0
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:
- Make the shader
- Add the shader file (.fx) to the Content Project.
- Make an instance of the Effect class
- Initiate the instance of the Effect class.
- Begin the shader and select what technique you want to use
- Pass different parameters to the shader
- Draw the scene/object
The steps in a bit more detail:
1.When making a shader, several programs like notepad, visual studio editor and so on can be used. There are also some shader IDEs available, and personally I like to use nVidias FX Composer: http://developer.nvidia.com/object/fx_composer_home.html
2. When the shader is created, drag it into the ”Content” colder, so it gets an asset name:
1.When making a shader, several programs like notepad, visual studio editor and so on can be used. There are also some shader IDEs available, and personally I like to use nVidias FX Composer: http://developer.nvidia.com/object/fx_composer_home.html
2. When the shader is created, drag it into the ”Content” colder, so it gets an asset name:
Shader is the asset name of the shader you added to the Contents folder.
3. XNA Framework includes a Effect class that is used to load and compile the shaders. To make an instance of this class, write the following line of code:
Effect effect;
Effect is a part of the “Microsoft.Xna.Framework.Graphics” library, so remember to add this line of code to the using statement block:
using Microsoft.Xna.Framework.Graphics
4. To initiate the shader, we can use Content to either load if from the project or from a file:
effect = Content.Load("Shader");
5. Select what technique you want to use:
effect.CurrentTechnique = effect.Techniques["AmbientLight"];
Also, you must start all the passes in the shader.
foreach (EffectPass pass in effect.CurrentTechnique.Passes) { // Begin the pass (index value - here 0 - selects the pass) effect.CurrentTechnique.Passes[0].Apply();
6. There are many ways to set a Shader parameter, but the following is sufficient for the tutorial. Note: This is not the fastest way of doing this, and I will come back to this in a later tutorial:
effect.Parameters["matWorldViewProj"].SetValue(worldMatrix * viewMatrix * projMatrix);
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.
7. Render the scene/object you want this shader to process/transform.
To understand this better, open the source code provided and see the steps in action.
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.
7. Render the scene/object you want this shader to process/transform.
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, lets start implementing the shader. First of all, we need a matrix that represents the world matrix:
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, lets 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.
Lets take a look at the vertex shader:
Lets take a look at the vertex shader:
OUT VertexShaderFunction( float4 Pos: POSITION ) { OUT Out = (OUT) 0; Out.Pos = mul(Pos, matWorldViewProj); return Out; }
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:
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:
float4 PixelShaderFunction() : COLOR { float Ai = 0.8f; float4 Ac = float4(0.075, 0.075, 0.2, 1.0); return Ai * Ac; }
Here we use 1.1 to calculate what the color of the current pixel will be. Ai is the ambient intensity, and Ac is the ambient color.
Last, we must define the technique and bind the pixelshader and certex shader function to the technique:
Last, we must define the technique and bind the pixelshader and certex shader function to the technique:
technique AmbientLight { pass P0 { VertexShader = compile vs_1_1 VertexShaderFunction(); PixelShader = compile ps_1_1 PixelShaderFunction(); } }
Ok, thats it!
Now, i recommend you to look at the source code and play around with the values in order to understand how to setup and implement a shader using XNA.
Download Sample
Now, i recommend you to look at the source code and play around with the values in order to understand how to setup and implement a shader using XNA.
Download Sample