May 29, 2009

XNA Game Engine Tutorial #12 – Introduction to HLSL and Improved Terrain

In this episode of the Game Engine Tutorial (we’ll return to the editor next week) we are going to build a better terrain, but the emphasis is more on implementing some simple HLSL effects.

We will cover:

- Better terrain construction
- Texture mapping
- Multiple texture mapping
- Directional lighting
- Fog

Better Terrain Construction

Our first shot at terrain was pretty good, but we are going to improve it here. The results will be completely unnoticable in the final image, but the functions for creating the terrain will be much better and more understandable. First, we need a new terrain class. I called mine Terrain2, but you can come up with something more creative. We will have it inherit from Component and I3DComponent, and add the standard values I3DComponent expects. They are of course filler values, because the terrain can’t rotate or scale or move to keep physics consistent.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Terrain2 : Component, I3DComponent
{
    // Setting [Browsable(false)] will make the property not show up in a properties box (like the one in the editor)
    [Browsable(false)]
    public Vector3 Position { get; set; }
 
    [Browsable(false)]
    public Vector3 EulerRotation { get; set; }
 
    [Browsable(false)]
    public Matrix Rotation { get; set; }
 
    [Browsable(false)]
    public Vector3 Scale { get; set; }
 
    [Browsable(false)]
    public BoundingBox BoundingBox { get; set; }
}

Next we will add the values that help shape the terrain. We store the height values of the terrain, a float to scale them by.

1
2
3
4
5
float[,] heights;
float heightMod = 1;
VertexBuffer vertexBuffer;
IndexBuffer indexBuffer;
VertexDeclaration vertexDeclaration;

The last thing we need to store is our physics HeightMapObject, which acts as the interface between the raw height value and a physics simulation of the terrain based on those heights.

1
HeightMapObject physicsHeightmap;

Before I go any further, I want to point out that the functions for generating the vertices, indices, and vertex normals are based on those given in the book XNA 3.0 Game Programming Recipes, by Riemer Grootjans (http://www.riemers.net).

The function below builds the terrain with a two dimesional array (X, Z) of heights (Y values). It also takes a scaling float that it multiplies by all the heights to resize the terrain on the Y axis.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void PopulateBuffers(float[,] heights, float heightMod)
{
    this.heights = heights;
 
    if (heights == null)
        return;
 
    // Offset the heights entered by the height scaling value
    float[,] heightsModded = new float[heights.GetLength(0), heights.GetLength(1)];
 
    for (int z = 0; z < heights.GetLength(1); z++)
        for (int x = 0; x < heights.GetLength(0); x++)
            heightsModded[x, z] = heights[x, z] * heightMod;
 
    // Generate the vertices and indices
    VertexPositionNormalTexture[] vertices = BuildVertices(heightsModded);
    int[] indices = BuildIndices(heightsModded);
 
    // Generate normals for the index buffer
    vertices = GenerateNormalsForTriangleStrip(vertices, indices);
 
    // Create the vertex declaration
    vertexDeclaration = new VertexDeclaration(Engine.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
 
    // Create the vertex buffer with the generated vertices
    vertexBuffer = new VertexBuffer(Engine.GraphicsDevice, VertexPositionNormalTexture.SizeInBytes * vertices.Length, BufferUsage.WriteOnly);
    vertexBuffer.SetData(vertices);
 
    // Create the index buffer with the generated indices
    indexBuffer = new IndexBuffer(Engine.GraphicsDevice, typeof(int), indices.Length, BufferUsage.WriteOnly);
    indexBuffer.SetData(indices);
 
    float highest = 0;
 
    // Find the highest vertex
    for (int x = 0; x < heights.GetLength(0); x++)
        for (int z = 0; z < heights.GetLength(1); z++)
            if (heightsModded[x, z] > highest)
                highest = heightsModded[x, z];
 
    // Rebuild the BoundingBox centered around (0, 0, 0), with ends at 1/2 the width and depth of the terrain, and as high as the highest (scaled) vertex
    BoundingBox = new BoundingBox(new Vector3(-heights.GetLength(0) / 2, 0, heights.GetLength(1) / 2), new Vector3(heights.GetLength(0) / 2, highest, -heights.GetLength(1) / 2));
 
    // Rebuilt the physics heightmap simulation object
    CreatePhysicsHeightmap(heightsModded);
}

The function above relies on two functions to build the vertices and indices. If you want more information on these functions, Riemer’s website has much more information:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// Builds the vertices for the terrain based on the given heights
VertexPositionNormalTexture[] BuildVertices(float[,] heights)
{
    // Find the width and depth of the terrain by getting the lengths of the heights array
    int width = heights.GetLength(0);
    int depth = heights.GetLength(1);
 
    // Create the list of vertices
    VertexPositionNormalTexture[] vertices = new VertexPositionNormalTexture[width * depth];
 
    int i = 0;
 
    // Move from front to back, row by row from the left to the right, creating the vertices and UV values
    for (int z = 0; z < depth; z++)
        for (int x = 0; x < width; x++)
        {
            Vector3 position = new Vector3(x - width / 2, heights[x, z], -z + depth / 2);
 
            // Temporary normal will be recalculated later
            Vector3 normal = new Vector3(0, 0, 0);
 
            // Since we want a square texture to completely fill the terrain,
            Vector2 uv = new Vector2((float)x / (float)width, (float)z / (float)depth);
 
            vertices[i++] = new VertexPositionNormalTexture(position, normal, uv);
        }
 
    return vertices;
}
 
// Generates the indices for the heights given
int[] BuildIndices(float[,] heights)
{
    int width = heights.GetLength(0);
    int depth = heights.GetLength(1);
 
    int[] indices = new int[width * 2 * (depth - 1)];
 
    int i = 0;
    int z = 0;
 
    while (z < depth - 1)
    {
        for (int x = 0; x < width; x++)
        {
            indices[i++] = x + z * width;
            indices[i++] = x + (z + 1) * width;
        }
 
        z++;
 
        if (z < depth - 1)
        {
            for (int x = width - 1; x >= 0; x--)
            {
                indices[i++] = x + (z + 1) * width;
                indices[i++] = x + z * width;
            }
        }
 
        z++;
    }
 
    return indices;
}
 
// Calculates normals for the given list of vertices and indices
VertexPositionNormalTexture[] GenerateNormalsForTriangleStrip(VertexPositionNormalTexture[] vertices, int[] indices)
{
    for (int i = 0; i < vertices.Length; i++)
        vertices[i].Normal = new Vector3(0, 0, 0);
 
    bool swappedWinding = false;
 
    for (int i = 2; i < indices.Length; i++)
    {
        Vector3 firstVec = vertices[indices[i - 1]].Position - vertices[indices[i]].Position;
        Vector3 secondVec = vertices[indices[i - 2]].Position - vertices[indices[i]].Position;
 
        Vector3 normal = Vector3.Cross(firstVec, secondVec);
        normal.Normalize();
 
        if (swappedWinding)
            normal *= -1;
 
        if (!float.IsNaN(normal.X))
        {
            vertices[indices[i]].Normal += normal;
            vertices[indices[i - 1]].Normal += normal;
            vertices[indices[i - 2]].Normal += normal;
        }
 
        swappedWinding = !swappedWinding;
    }
 
    for (int i = 0; i < vertices.Length; i++)
        vertices[i].Normal.Normalize();
 
    return vertices;
}

The function we used to build the terrain also relies on another function to create the physics heightmap object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Rebuilds the HeightMapObject from the given heights
void CreatePhysicsHeightmap(float[,] heights)
{
    // If the physics object has been created already with previous settings...
    if (physicsHeightmap != null)
    {
        // And there is a physics simulator available
        Physics p = Engine.Services.GetService
();
        if (p != null)
        {
            // Disable the old height map object and remove it from the engine
            physicsHeightmap.Body.DisableBody();
            p.PhysicsSystem.RemoveBody(PhysicsHeightmap.Body);
            physicsHeightmap.DisableComponent();
        }
    }
 
    // Create the new HeightMapObject
    physicsHeightmap = new HeightMapObject(new JigLibX.Geometry.HeightMapInfo(heights, 1), new Vector2(-1.0f, -1.0f), Engine);
}

OK, so far so good. Now we can build a terrain from a list of height values. We could give it values to work with by hand, but that would take forever, so we will add a way to build the terrain from a texture. The texture should be a gray scale map, and we will take the values, which will range from 0 – 1, and create an array of heights from all the pixels in the image.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Accepts a texture and extracts the height values from it to create an array of heights
void BuildTerrainFromTexture(Texture2D Texture, float heightMod)
{
    Texture2D t = Texture;
 
    // Extract the pixel values into an array of Colors
    Color[] colors = new Color[t.Width * t.Height];
    t.GetData(colors);
 
    // Create a two dimensional array of heights
    float[,] heights = new float[t.Width, t.Height];
 
    // Move from left to right,
    for (int x = 0; x &lt; t.Width; x++)
        for (int z = 0; z &lt; t.Height; z++)
            heights[x, z] = colors[x + z * t.Width].R / 5f;
 
    PopulateBuffers(heights, heightMod);
}

Let’s add a couple of public properties so that we can set these values from a PropertyGrid (in the editor).

1
2
3
4
5
6
7
8
9
10
11
12
13
string heightmap;
 
public string Heightmap
{
    get { return heightmap;}
    set { heightmap = value; BuildTerrainFromTexture(Engine.Content.Load(value), heightMod); }
}
 
public float HeightMod
{
    get { return heightMod; }
    set { heightMod = value; if (heights != null) PopulateBuffers(heights, value); }
}

Now let’s work on our Draw() method. Drawing the terrain is pretty simple, but we need an effect. For now we’ll just use BasicEffect.

1
2
3
4
Effect effect;
 
// In the constructor
effect = new BasicEffect(Engine.GraphicsDevice, null);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public override void Draw()
{
    Camera c = Engine.Services.GetService();
 
    if (c == null || heights == null || vertexBuffer == null || indexBuffer == null || vertexDeclaration == null)
        return;
 
    int width = heights.GetLength(0);
    int depth = heights.GetLength(1);
 
    // We need to flip the terrain across the z axis to make it fit the physics heightmap
    effect.Parameters["World"].SetValue(Matrix.CreateScale(1, 1, -1));
    effect.Parameters["View"].SetValue(c.View);
    effect.Parameters["Projection"].SetValue(c.Projection);
 
    ((BasicEffect)effect).EnableDefaultLighting();
 
    // The normals will be inverted by the z-flip so we need to switch the cullmode around to draw properly
    Engine.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;
 
    effect.Begin();
 
    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Begin();
 
        // Set the vertex source, index source, and VertexDeclaration
        Engine.GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes);
        Engine.GraphicsDevice.Indices = indexBuffer;
        Engine.GraphicsDevice.VertexDeclaration = vertexDeclaration;
 
        // Draw a TriangleStrip using the vertices and indices we set
        Engine.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, width * depth, 0, width * 2 * (depth - 1) - 2);
 
        pass.End();
    }
 
    effect.End();
 
    // Set the cullmode back
    Engine.GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
}

Run it, and you should see a nice… silver shiny terrain.

Terrain #1

Let’s make our own effect to replace BasicEffect. Add a new effect to your content project. (Add > New Item > Effect). You will be taken to the HLSL editor. Leave it the way it is for now and go back to your terrain. Change the line where we load the effect to:

1
effect = Engine.Content.Load("Content/TerrainEffect"); // Or whatever your effect is called

Next remove the following from the Draw() method:

1
((BasicEffect)effect).EnableDefaultLighting();

If you run it again, you’ll see that your effect has been applied. Of course, we haven’t done anything with it yet, so you’ll just see a big red blob.

Terrain #2

Intro to HLSL

Before we can do more with the shader we need to understand a few things about HLSL. HLSL is the programming language for GPUs. There are other languages (like GLSL in OpenGL), but DirectX, and thus XNA, uses HLSL. HLSL stands for “High Level Shader Language”. The .fx file is a “Shader”, because it is responsible for taking in values form the program and GPU and deciding what color to output (shade) each pixel on the screen.

We’ll analyze each part of an HLSL file before we use it. First, have a look at the first chunk of code. These are your effect parameters. They are basically chunks of memory that sit on the graphics card, which it can access. However, the CPU can also access these parameters, which means we can share data between the two. (Although it is usually one way, because it is usually very, VERY, slow to read from the GPU. You should avoid this at all costs.)

In HLSL, a float4×4 is equivelent to XNA’s Matrix. float2, float3, and float4 are Vector2, Vector3, and Vector4. float, int, and bool are the same. texture is a Texture2D object.

1
2
3
float4x4 World;
float4x4 View;
float4x4 Projection;

We have already shown how to set these parameters from XNA in our draw method. Now you can see what is actually happening when we set parameters on the Effect like in the draw method:

1
2
3
effect.Parameters["World"].SetValue(Matrix.CreateScale(1, 1, -1));
effect.Parameters["View"].SetValue(c.View);
effect.Parameters["Projection"].SetValue(c.Projection);

Next in the HLSL are some struct definitions:

1
2
3
4
5
6
7
8
9
struct VertexShaderInput
{
    float4 Position : POSITION0;
};
 
struct VertexShaderOutput
{
    float4 Position : POSITION0;
};

These structs are used to send data between the GPU and the shader’s functions. You can have more than two, and use them for different things (for example you could have several versions of a shader, but if we look ahead you can see that the first function takes in a VertexShaderInput struct, and outputs a VertexShaderOutput struct, and the second functions accepts a VertexShaderOutput struct. values in an HLSL struct like these need to have a “Shader Semantic” assigned to them. This is done by adding a “: SemanticName” after the name of the parameter. For example, the Position vector is assigned to POSITION0, which can also be just POSITION. There are a lot of different semantics, but we will just use a few of them. I should note that the TEXCOORD semantic, which stores the UV layout of the input vertex, has several “layers”, ie: TEXCOORD0, TEXCOORD1, TEXCOORD4. TEXCOORD0 usually holds the UV information, but the rest are available to store other data, like light direction, normal, or whatever. There are a llimited number available, and that number changes from shader version to shader version, but there are usually enough to get most effects done. I’ll talk more about shader versions at the end of this section.

The next section of code is the “Vertex Shader”. This is a function that is primarily responsible for filling out the POSITION semantic, but a lot of other work can be done there too. For example, texture coordinate mapping, vertex coloring and lighting is handled here. The struct that is passed in to it will be filled out by the GPU, passing the information the vertex shader needs from the data the GPU is tracking.

1
2
3
4
5
6
7
8
9
10
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
 
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
 
    return output;
}

In the example above, the shader is taking in the position of the vertex in “World Space”, offsetting it by the World matrix, then transforming it based on the View and Projection matrices. The View matrix basically moves the vertex based on the camera transformation, and the projection matrix “warps” the display so that it fits the screen, aspect ration, etc.

The second function is the “Pixel Shader”. It works on a pixel by pixel basis, taking in information from the vertex shader, such as position, texture coordinates, etc. and deciding what color the output pixel should be. In this example, it is just outputting the color red (255, 0, 0, 255) (Colors are shrunk from 0-255 to 0-1 in HLSL. The fourth parameter is transparency).

1
2
3
4
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return float4(1, 0, 0, 1);
}

The last part of a shader is the definition of techniques. A technique is basically a list of “passes”, which define a vertex shader and pixel shader inside each of them. Normally there is only one pass, but for advanced shading techniques more than one can be used. The vertex and pixel shader definitions inside the technique define a pixel shader version and vertex shader version. These are used when compiling the shader (which XNA does automatically in the content build before running the game). By default they are vertex shader 1.0 (vs_1_1) and pixel shader 1.0 (ps_1_1). Most cards support pixel shader 2.0 and most cards also support 3.0. The difference is mainly in the number of available instructions and semantics. The instruction count is the number of things that can be done in a shader. Available semantics change from version to version, but the basic ones are available in all the versions XNA can use. You can define a number of different techniques, and use them based on available hardware capabilities for example.

HLSL Texture Mapping

Now that you have an understanding of HLSL, lets do something with it by adding a texture to the terrain. Add the following properties:

1
2
texture TextureMap;
sampler2D TextureMapSampler = sampler_state { texture = ; AddressU = CLAMP; AddressV = CLAMP; MinFilter = LINEAR; MipFilter = LINEAR; MagFilter = LINEAR; };

The first property is simple the Texture2D we want to use, but the second is a texture “sampler” this is used to extract the correct part of the texture when applying it to a pixel in the pixel shader. We can set a number of properties, like texture filtering, which handles the resizing of textures based on distince. “Linear” means it will do a straight resize. “Linear” is the lowest quality, but it still looks fine. The highest is “Anisotropic”, which takes the most processing power, but looks the best. The wrap mode (“AddressU”, “AddressV”), which tells the sampler what to do when the UV coordinate of the texture is great than 1 or less than 0. “Clamp” means that it will continue to use the texture at UV (1), or the edge of the texture. If you specified “Wrap” instead of “Clamp”, the texture would wrap back around and repeat. We also specify the texture that the sampler is sampling from.

We need to add the texture property to the C# code as well, and set it in the draw function.

1
2
3
4
5
6
7
8
string textureMap;
Texture2D texMap;
 
public string TextureMap
{
    get { return textureMap; }
    set { textureMap = value; texMap = Engine.Content.Load(value); }
}
1
2
if (texMap != null)
    effect.Parameters["TextureMap"].SetValue(texMap);

Now we need to modify the shader’s vertex shader input/output structs, the vertex shader, and the pixel shader to draw the texture onto the terrain. We also need to move to Pixel Shader 2.0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
float4x4 World;
float4x4 View;
float4x4 Projection;
 
texture TextureMap;
sampler2D TextureMapSampler = sampler_state { texture = ; AddressU = CLAMP; AddressV = CLAMP; MinFilter = LINEAR; MipFilter = LINEAR; MagFilter = LINEAR; };
 
struct VertexShaderInput
{
    float4 Position : POSITION0;
    float2 UV : TEXCOORD0;
};
 
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 UV : TEXCOORD0;
};
 
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
 
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
 
    output.UV = input.UV;
 
    return output;
}
 
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
	float4 tex = tex2D(TextureMapSampler, input.UV);
 
    return tex;
}
 
technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_1_1 VertexShaderFunction();
        PixelShader = compile ps_1_1 PixelShaderFunction();
    }
}

Now, set the TextureMap property on the terrain to your image, run it, and you should see it spread over the terrain.

grass

Terrain #3

uvs

Well, it’s a start but it looks pretty pixelated and blurry. Let’s make it repeat over the terrain instead of being stretched across it. We will use a simple float value and multiply the UVs by it. To better understand this, look at the diagram below to see how changing the UV values will change the way the texture is layed across it.

C#:

1
2
3
4
5
6
7
8
9
10
float uvScaling = 30.0f; // Repeat 30 times across the terrain
 
public float UVScaling
{
    get { return uvScaling; }
    set { uvScaling = value; }
}
 
// Draw method
effect.Parameters["UVScaling"].SetValue(uvScaling);

HLSL

1
2
3
4
float UVScaling = 30.0f;
 
// Pixel shader
float4 tex = tex2D(TextureMapSampler, input.UV * UVScaling);

Now if you run this you will see that the texture is tiled across the terrain.

Terrain 4

Multiple Texture Mapping

This looks OK, but let’s extend it a little bit. We will change this so that we can have four textures on the terrain, laid out by a fifth’s Red, Green, Blue, and Alpha channels. The texture map will be stretched across the terrain, and the others will tile.

C#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
float4 red = tex2D(RedSampler, input.UV * UVScaling);
float4 green = tex2D(GreenSampler, input.UV * UVScaling);
float4 blue = tex2D(BlueSampler, input.UV * UVScaling);
float4 alpha = tex2D(AlphaSampler, input.UV * UVScaling);
 
public string RedTexture
{
    get { return redTexture; }
    set { redTexture = value; if (string.IsNullOrEmpty(value)) rTex = null; else rTex = Engine.Content.Load(value); }
}
 
public string GreenTexture
{
    get { return greenTexture; }
    set { greenTexture = value; if (string.IsNullOrEmpty(value)) gTex = null; else gTex = Engine.Content.Load(value); }
}
 
public string BlueTexture
{
    get { return blueTexture; }
    set { blueTexture = value; if (string.IsNullOrEmpty(value)) bTex = null; else bTex = Engine.Content.Load(value); }
}
 
public string AlphaTexture
{
    get { return alphaTexture; }
    set { alphaTexture = value; if (string.IsNullOrEmpty(value)) aTex = null; else aTex = Engine.Content.Load(value); }
}
 
// Draw method
if (rTex != null)
    effect.Parameters["RedTexture"].SetValue(rTex);
 
if (gTex != null)
    effect.Parameters["GreenTexture"].SetValue(gTex);
 
if (bTex != null)
    effect.Parameters["BlueTexture"].SetValue(bTex);
 
if (aTex != null)
    effect.Parameters["AlphaTexture"].SetValue(aTex);

C#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
texture RedTexture;
sampler2D RedSampler = sampler_state { texture = ; AddressU = WRAP; AddressV = WRAP; MinFilter = LINEAR; MipFilter = LINEAR; MagFilter = LINEAR; };
 
texture GreenTexture;
sampler2D GreenSampler = sampler_state { texture = ; AddressU = WRAP; AddressV = WRAP; MinFilter = LINEAR; MipFilter = LINEAR; MagFilter = LINEAR; };
 
texture BlueTexture;
sampler2D BlueSampler = sampler_state { texture = ; AddressU = WRAP; AddressV = WRAP; MinFilter = LINEAR; MipFilter = LINEAR; MagFilter = LINEAR; };
 
texture AlphaTexture;
sampler2D AlphaSampler = sampler_state { texture = ; AddressU = WRAP; AddressV = WRAP; MinFilter = LINEAR; MipFilter = LINEAR; MagFilter = LINEAR; };
 
// Pixel shader
float4 red = tex2D(RedSampler, input.UV * UVScaling);
float4 green = tex2D(GreenSampler, input.UV * UVScaling);
float4 blue = tex2D(BlueSampler, input.UV * UVScaling);
float4 alpha = tex2D(AlphaSampler, input.UV * UVScaling);
 
float4 map = tex2D(TextureMapSampler, input.UV);
 
float a = map.a;
float4 final = (map.r * a * red) + (map.g * a * green) + (map.b * a * blue) + ((1.0f - a) * alpha);return final;

Set those properties on the terrain, run it, and you will see the textures are spread across the terrain.

dirt grass texmap1 sand rock

Terrain #5

Directional Lighting

Our textures look nice, but everything looks really flat. Now we are going to add some simple ambient lighting, and directional lighting. Ambient lighting is basically a lowish light value that is multiplied by the texture, to simulate areas that are not lit directly but still are not completely black because of the way light bounces around. To perform directional lighting, we multiply the texture by the dot product of the light direction and vertex normal. Add the following parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Vector3 lightDirection = new Vector3(1, 1, 1);
Vector3 ambientColor = new Vector3(.2f, .2f, .2f);
Vector3 lightColor = new Vector3(1, 1, 1);
 
public Vector3 LightDirection
{
    get { return lightDirection; }
    set { lightDirection = value; }
}
 
public Vector3 AmbientColor
{
    get { return ambientColor; }
    set { ambientColor = value; }
}
 
public Vector3 LightColor
{
    get { return lightColor; }
    set { lightColor = value; }
}
 
// Draw method
effect.Parameters["LightDirection"].SetValue(LightDirection);
effect.Parameters["Ambient"].SetValue(AmbientColor);
effect.Parameters["LightColor"].SetValue(LightColor);

HLSL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct VertexShaderInput
{
    float4 Position : POSITION0;
    float2 UV : TEXCOORD0;
    float3 Normal : NORMAL0;
};
 
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 UV : TEXCOORD0;
    float3 Normal : TEXCOORD1;
};
 
// Add to vertex shader
output.Normal = mul(input.Normal, World);
 
// Add to pixel shader
float3 normal = normalize(input.Normal);
float3 lightDir = normalize(LightDirection);
float3 lightMod = dot(normal, lightDir);
 
lightMod.r = clamp(lightMod.r * LightColor.r, Ambient.r, 1);
lightMod.g = clamp(lightMod.g * LightColor.g, Ambient.g, 1);
lightMod.b = clamp(lightMod.b * LightColor.b, Ambient.b, 1);
 
final *= float4(lightMod, 1);
grass texmap2

Terrain #6

Fog
We are going to add one more effect to our shader. Fog can look very realistic, especially on terrain. It can also be useful for a multitude of other things- weather effects, light flares, etc. Don’t believe me? Just ask XNA Framework Developer Shawn Hargreaves (http://blogs.msdn.com/shawnhar). In his series on MotoGP, he describes 5 or 6 ways they used the fog system, other than just fog.

To calculate fog, just pick a start point, end point, and density. Then, in the pixel shader, you figure out how far away the pixel is from the camera, figure out how far into the fog it is, and multiply that by the fog density, then fog color.

C#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public float FogDensity { get; set; }
public float MaxFog { get; set; }
public float MinFog { get; set; }
public float FogStart { get; set; }
public float FogEnd { get; set; }
public Vector3 FogColor { get; set; }
 
// Draw function
if (Data.ContainsData("Terrain.FogStart"))
    FogStart= Data.GetData("Terrain.FogStart");
 
if (Data.ContainsData("Terrain.FogEnd"))
    FogEnd = Data.GetData("Terrain.FogEnd");
 
if (Data.ContainsData("Terrain.FogDensity"))
    FogDensity = Data.GetData("Terrain.FogDensity");
 
if (Data.ContainsData("Terrain.FogColor"))
    FogColor = Data.GetData("Terrain.FogColor");
 
if (Data.ContainsData("Terrain.MaxFog"))
    MaxFog = Data.GetData("Terrain.MaxFog");
 
if (Data.ContainsData("Terrain.MinFog"))
    MinFog = Data.GetData("Terrain.MinFog");

HLSL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct VertexShaderOutput
{
    float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
	float3 Normal : TEXCOORD1;
	float Depth : TEXCOORD2;
};
 
// Add to vertex shader
output.Depth = output.Position.z;
 
// Add to pixel shader
float depth = input.Depth;
 
float fog = (depth - FogStart) / (FogEnd - FogStart);
fog *= FogDensity;
fog = clamp(fog, MinFog, MaxFog);
 
final = lerp(final, float4(FogColor, 1), fog);

Image #7

The last thing I’m going to throw in at the end here are the serialization and deserialization functions for this terrain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public override void SaveSerializationData(SerializationData Data)
{
    base.SaveSerializationData(Data);
 
    if (AlphaTexture != null)
        Data.AddData("Terrain.AlphaTexture", AlphaTexture);
 
    if (RedTexture != null)
        Data.AddData("Terrain.RedTexture", RedTexture);
 
    if (GreenTexture != null)
        Data.AddData("Terrain.GreenTexture", GreenTexture);
 
    if (BlueTexture != null)
        Data.AddData("Terrain.BlueTexture", BlueTexture);
 
    if (TextureMap != null)
        Data.AddData("Terrain.TextureMap", TextureMap);
 
    if (Heightmap != null)
        Data.AddData("Terrain.Heightmap", Heightmap);
 
    Data.AddData("Terrain.UVScaling", uvScaling);
    Data.AddData("Terrain.LightDirection", LightDirection);
    Data.AddData("Terrain.AmbientColor", AmbientColor);
    Data.AddData("Terrain.LightColor", LightColor);
    Data.AddData("Terrain.FogStart", FogStart);
    Data.AddData("Terrain.FogEnd", FogEnd);
    Data.AddData("Terrain.FogDensity", FogDensity);
    Data.AddData("Terrain.FogColor", FogColor);
    Data.AddData("Terrain.MinFog", MinFog);
    Data.AddData("Terrain.MaxFog", MaxFog);
 
    Data.AddData("Terrain.HeightMod", HeightMod);
}
 
public override void LoadFromSerializationData(SerializationData Data)
{
    base.LoadFromSerializationData(Data);
 
    if (Data.ContainsData("Terrain.HeightMod"))
        HeightMod = Data.GetData("Terrain.HeightMod");
 
    if (Data.ContainsData("Terrain.Heightmap"))
        Heightmap = Data.GetData("Terrain.Heightmap");
 
    if (Data.ContainsData("Terrain.AlphaTexture"))
        AlphaTexture = Data.GetData("Terrain.AlphaTexture");
 
    if (Data.ContainsData("Terrain.RedTexture"))
        RedTexture = Data.GetData("Terrain.RedTexture");
 
    if (Data.ContainsData("Terrain.GreenTexture"))
        GreenTexture = Data.GetData("Terrain.GreenTexture");
 
    if (Data.ContainsData("Terrain.BlueTexture"))
        BlueTexture = Data.GetData("Terrain.BlueTexture");
 
    if (Data.ContainsData("Terrain.TextureMap"))
        TextureMap = Data.GetData("Terrain.TextureMap");
 
    if (Data.ContainsData("Terrain.UVScaling"))
        uvScaling = Data.GetData("Terrain.UVScaling");
 
    if (Data.ContainsData("Terrain.LightDirection"))
        LightDirection = Data.GetData("Terrain.LightDirection");
 
    if (Data.ContainsData("Terrain.AmbientColor"))
        AmbientColor = Data.GetData("Terrain.AmbientColor");
 
    if (Data.ContainsData("Terrain.LightColor"))
        LightColor = Data.GetData("Terrain.LightColor");
 
    if (Data.ContainsData("Terrain.FogStart"))
        FogStart= Data.GetData("Terrain.FogStart");
 
    if (Data.ContainsData("Terrain.FogEnd"))
        FogEnd = Data.GetData("Terrain.FogEnd");
 
    if (Data.ContainsData("Terrain.FogDensity"))
        FogDensity = Data.GetData("Terrain.FogDensity");
 
    if (Data.ContainsData("Terrain.FogColor"))
        FogColor = Data.GetData("Terrain.FogColor");
 
    if (Data.ContainsData("Terrain.MaxFog"))
        MaxFog = Data.GetData("Terrain.MaxFog");
 
    if (Data.ContainsData("Terrain.MinFog"))
        MinFog = Data.GetData("Terrain.MinFog");
}


38 Responses to “XNA Game Engine Tutorial #12 – Introduction to HLSL and Improved Terrain”

  1.   nikescar Says:
      May 30, 2009 at 9:19 am

    This is the easiest to understand multitexturing tutorial I’ve yet seen. I can’t wait to try it out.

  2.   Dave1005 Says:
      May 30, 2009 at 3:16 pm

    First off, great tutorial!
    My question is: what is the GenerateNormalsForTriangleStrip method referring to? I can not run the game because of it and can not find the method anywhere.

  3.   Dave1005 Says:
      May 30, 2009 at 3:45 pm

    Nevermind. I realized it is a method from the Terrain class. What exactly is taken from the Terrain class and what is not? Is there anyway you can post this new terrain class? Thanks.

  4.   Sean Says:
      May 30, 2009 at 4:57 pm

    No, everything is a new class, I just forgot to include GenerateNormalsForTriangleStrip. It’s been updated now.

  5.   Dave1005 Says:
      May 30, 2009 at 5:47 pm

    Thanks Sean. Is there anything special to add to the constructor? Because when I try to use the new Terrain, I don’t have anything rendering.

  6.   Sean Says:
      May 30, 2009 at 5:49 pm

    You need to set at least the heightmap, a texture MAP, and at least one texture. To start off you can just set a solid red texture as the map, and a grass or something to the red slot, then add a heightmap and you will see something, like in the last two examples.

  7.   Jon Says:
      May 31, 2009 at 1:55 am

    Hey Sean. Cheers for the awesome tutorials! Seems like wordpress has maybe nommed some more &tl; > bits? getting errors in CreatePhysicsHeightMap

  8.   Jon Says:
      May 31, 2009 at 2:00 am

    fixed?
    Engine.Serivces.GetService<Physics>();

    and
    Engine.DefaultScreen
    as the last parameter for
    physicsHeightmap = new HeightMapObject…

    errors are gone, but not sure if i’ve guessed bits right :P

  9.   Dave1005 Says:
      May 31, 2009 at 8:42 am

    In the .fx file, in the line:
    sampler2D TextureMapSampler = sampler_state { texture = ; AdressU = CLAMP; AddressV = CLAMP; MinFilter = LINEAR; MagFilter = LINEAR; }
    what should “texture =” be set equal to?

  10.   Dave1005 Says:
      May 31, 2009 at 8:44 am

    Sorry. I must be getting annoying. Figured out what my error meant. :)

  11.   Dave1005 Says:
      May 31, 2009 at 10:48 am

    But now I can’t fix it. Sorry. It is referring to the same line, but the error says:
    Error 2 Errors compiling C:\Users\David\Documents\Visual Studio 2008\C#\Projects\Illuzion\Illuzion\Content\AdvancedTerrain\AdvancedTerrain.fx:
    C:\Users\David\Documents\Visual Studio 2008\C#\Projects\Illuzion\Illuzion\Content\AdvancedTerrain\AdvancedTerrain.fx(6): error X3000: syntax error: unexpected token ‘;’ C:\Users\David\Documents\Visual Studio 2008\C#\Projects\Illuzion\Illuzion\Content\AdvancedTerrain\AdvancedTerrain.fx 6 1 Illuzion
    Any ideas. I triple checked what I typed and can’t figure it out.

  12.   Jon Says:
      May 31, 2009 at 12:04 pm

    sampler2D TextureMapSampler = sampler_state { texture = <TextureMap>;

    Another victim of wordpress’ safe html parsing. the rest of the line is fine.

  13.   Jon Says:
      May 31, 2009 at 12:07 pm

    Hmm, I’m failing to get the terrain texturing atm. VS is throwing up no errors right now, but even though i have a Terrain2 declared (terr), and a HeightMap and TextureMap specified, it seems to ignore the texture… Just renders the terrain all black (PS 2 and 3) or all white (PS 1.1)

    Changing the HLSL stuff to apply a colour rather than the texture doesn’t seem to work either…

  14.   Jon Says:
      May 31, 2009 at 1:41 pm

    yay, texturing works. in your multitexturing section sean, there are some bits of HLSL mixed in with C# code sections.

    Think you accidentally copied some float4 declarations instead of some strings or other such that belong in the C# ;)

  15.   Dave1005 Says:
      May 31, 2009 at 3:05 pm

    Is there some way you could post your code somewhere, Jon? I have a feeling mine is completely messed up and just want a reference to make sure my class is set up correctly. If you want to email me, you can send it to 3dcg.drich@gmail.com. Thanks!

  16.   Dave1005 Says:
      May 31, 2009 at 3:05 pm

    Sorry. It’s 3dgc.drich@gmail.com :)

  17.   Jon Says:
      June 1, 2009 at 1:37 pm

    Hi Dave, I’ve set up a blog (been meaning to for aaaages on xna) and have hosted my current source for the engine. It’s got some extra bits, but all the code for this tutorial is in and working, with the exception of fog, which i can’t quite get right. I’ve had to write my own Draw function for it, and obviously it’s not completely correct. either way: http://xactly.randomnation.co.uk for what i’m doing to the engine, and also some helpful XNA posts of my own, in the near future :)

  18.   Dave1005 Says:
      June 1, 2009 at 2:38 pm

    Thanks so much Jon. It was such a huge help. I’m also going to watch for blog posts on your site, because I am also very interested in audio management.

    Thanks again,
    Dave

  19.   Jon Says:
      June 1, 2009 at 3:32 pm

    No probs, glad I could help. and yeah, bear with me while i get posts going, but i will certainly be covering at least some basics soon ;)

  20.   Impersonation Says:
      June 3, 2009 at 10:18 pm

    Is it possible to update the “XNA Engine Tutorial link” to include Tutorial 12 as well?

  21.   Mal Says:
      June 6, 2009 at 5:09 am

    Hi guys I fixed the fog and clarified where everything should go, hopefully helps someone.

    C#
    // add the Terrain properties and values
    Vector3 fogColor = new Vector3(1, 1, 1);
    float fogDensity = 0.2f;
    float maxFog = 0.6f;
    float minFog = 0.1f;
    float fogStart = 30.1f;
    float fogEnd = 80.2f;

    public Vector3 FogColor {
    get { return fogColor; }
    set { fogColor = value; }
    }

    public float FogDensity {
    get{return fogDensity;}
    set { fogDensity = value; }
    }

    public float MaxFog {
    get { return maxFog; }
    set { maxFog = value; }
    }

    public float MinFog {
    get { return minFog; }
    set { minFog = value; }
    }

    public float FogStart {
    get { return fogStart; }
    set { fogStart = value; }
    }

    public float FogEnd {
    get { return fogEnd; }
    set { fogEnd = value; }
    }

    // Add In Terrain Draw method before the translation effect calls
    effect.Parameters["FogDensity"].SetValue(FogDensity);
    effect.Parameters["MaxFog"].SetValue(MaxFog);
    effect.Parameters["MinFog"].SetValue(MinFog);
    effect.Parameters["FogStart"].SetValue(FogStart);
    effect.Parameters["FogEnd"].SetValue(FogEnd);
    effect.Parameters["FogColor"].SetValue(FogColor);

    HLSL
    // HLSL add at top
    float3 FogColor;
    float FogDensity;
    float MaxFog;
    float MinFog;
    float FogStart;
    float FogEnd;

    // Change vertext shader output function to
    struct VertexShaderOutput
    {
    float4 Position : POSITION0;
    float2 UV : TEXCOORD0;
    float3 Normal : TEXCOORD1;
    float Depth : TEXCOORD2;
    };

    // Add to VertexShaderFunction
    output.Depth = output.Position.z;

    // Add to PixelShaderFunction just before the return final
    float depth = input.Depth;
    float fog = (depth – FogStart) /(FogEnd – FogStart);
    fog *= FogDensity;
    fog = clamp(fog, MinFog, MaxFog);
    final = lerp(final, float4(FogColor, 1), fog);

    Cheers
    Mal

  22.   Dave1005 Says:
      June 11, 2009 at 7:16 pm

    Has anyone else had a problem with the blur being used with this terrain? I am seeing through the terrain from some angles, but not from others. Also, should the fog be applied only to the terrain, or is it supposed to affect everything in the environment?

  23.   Sean Says:
      June 11, 2009 at 8:07 pm

    Make sure your renderstates are set correctly.

    The fog should only apply to the terrain, because we are only using that shader on the terrain. You would have to add the fog code to the rest of the shaders used in the scene to see it everywhere.

  24.   Dave1005 Says:
      June 13, 2009 at 1:56 pm

    Just wanted to make sure about the fog. Any ideas about the blur, though? I have pictures here:
    http://img10.imageshack.us/img10/7553/blur1.jpg
    http://img14.imageshack.us/img14/2718/blur2c.jpg
    Note that in the second one, you not only see through terrain at parts, but it also adds a black bar to the bottom of the screen.

    Thanks Sean!

  25.   Chris Harshman Says:
      June 14, 2009 at 1:00 pm

    Do you know any good references for making a terrain system that can take color map with more than 4 colors.

    That seems to be were the common knowledge ends in terms of anyone’s tutorials.

    In fact, I am looking for something that can teach me how to add bumpmap and a colormap at least to a heightmap with shaders.

  26.   Sean Says:
      June 14, 2009 at 4:56 pm

    You could use some pretty similar code to do more than 4 textures, but you would have to draw the terrain multiple times. Basically:

    - Keep a list of textures and texture maps.
    - Break the list into sets of 4 textures with their corresponding texture maps
    - For each set of textures, draw the terrain using the textures and texture map
    - While drawing, draw a completely transparent pixel when there is no texture mapped to it (black on the texture map)
    - Make sure transarency is enabled when drawing, so that the different ‘layers’ of terrains blend together

  27.   Chris Harshman Says:
      June 14, 2009 at 5:01 pm

    My colormap is not a reference, sorry forgot to add that, my color is 2048 by 2048 I can also make it any other size, and is the entire texture file for the heightmap, so I need to wrap the colormap over the heightmap.

  28.   Dave1005 Says:
      June 16, 2009 at 3:07 pm

    If anyone else has this problem, just add this to the draw method:

    Engine.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace; Already there.
    Engine.GraphicsDevice.RenderState.DepthBufferEnable = true;

    Good luck!

  29.   Aitor Says:
      June 18, 2009 at 6:14 pm

    Hi! I´m getting the same error than Ilya Ostrovskiy at tutorial 9 (http://i44.tinypic.com/2expl4i.png) but only with this tutorial (I draw ok Terrain class, but I get the error whith Terrain2 class when UVScaling > 1). What could be wrong?

    Thanks, and sorry about my english.

  30.   Aitor Says:
      June 19, 2009 at 4:55 am

    Don`t worry about my problem. I solve it whith:

    Engine.GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
    Engine.GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;

    after:

    pass.Begin();

  31.   BDAnders Says:
      July 13, 2009 at 7:12 pm

    As far as I can make out FogStart and FogEnd do not correlate to distances as you think of them mathematically in XNA, it’s more about separating similar distances from thoes who are closer and further away so the distances you see (especially when you have pixels that have “infinite” depth (the sky)) the values are less than linear. for the results above, you may want to try a FogStart slightly smaller than you think you want, and FogEnd like ten times what you think you want. From that you can adjust and get what you really want for your given setting,

    Every time you make a new setting with fog you’ll have to readjust these figured I imagine. I suppose a good idea would be to make a skybox component that would limit this odd disparity with the depth buffer, make it a bit more linear.

  32.   Shaneje Says:
      July 29, 2009 at 11:31 am


    Sean Says:
    June 14, 2009 at 4:56 pm

    You could use some pretty similar code to do more than 4 textures, but you would have to draw the terrain multiple times. Basically:

    Would you be able to create a short tutorial on this?
    I would love to be able to use more than 4 textures, and have them blending nicely, however I cannot get my head around it all!

    Any chance also of a code download for this chapter, I am doing many things wrong and can’t seam to get it to work lol!

  33.   Shaneje Says:
      July 30, 2009 at 10:03 am

    For some reason my terrain is rendering the texturemap tiled. By this I mean, rather than having sand in one corner, with grass painted over the rest, it is taking that and repeating it all over the terrain.

    ScreenShot Of Terrain:
    http://euphoricnation.com/images/terrain_texturemap_odd.jpg

    There is no errors/bugs and so I cannot seam to find the solution, I’ve had a tinker with the .fx file but have had no luck.

    Does anyone know why my terrain2 component is rendering like this?

    ps: I have also added the following lines after ‘pass.Begin()’…

    Engine.GraphicsDevice.RenderState.DepthBufferEnable = true;
    Engine.GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
    Engine.GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;

    ..However these have made no difference!

    Any ideas?
    Thanks in advance!

  34.   Shaneje Says:
      July 30, 2009 at 4:33 pm

    Its okay now, I have no idea what I did different this time but I recoded the terrain2 class from scratch. I must have either missed out something, or pershaps put something in the wrong order.

    Either way it is working now :D

  35.   Shaneje Says:
      July 30, 2009 at 7:14 pm

    Thought that Chris may like to see this following screen shot since he was on about multi-texturing more that just 4 textures.

    I finally have cracked the terrain problems I was having and now just finished a layering function for the textures on the terrain.

    http://www.euphoricnation.com/images/terrain_layered.jpg

  36.   Dylan Dalrymple Says:
      September 22, 2009 at 5:49 pm

    Hey, I used to follow X-Engine a while back (maybe 1.5 years). So, I decided to check back on it after that long. Then, I notice that you’ve got this totally new thing going and it looks really awesome. Therefore, I have to say that your decisions on what to do with X-Engine have gone a good way. Good job, and it looks great!

  37.   Eric Says:
      October 30, 2009 at 2:06 am

    Is it possible to download this example? i really need help in my project.

  38.   Shane Says:
      February 14, 2010 at 5:25 am

    Although this is a very late reply to Eric and that the comments have be quite for a long time but…

    ERIC: If you are on about a example project for my terrain with multiple textures, well I don’t have any of the old code anymore; I’ve moved between computers/laptops alot recently.

    However, I am currently working on a 3D terrain with LOD (quadtrees), and multi textures, Basically same display as the above, however the performance is alot greaters with such a lot less to render to the screen.

    All good, I have no idea how long it will take, ideally I wanted to make it for engine 2.0 on this website. However the tutorials are taking alot longer to come out than I first expected. I will probably make a simple project without the engine for now, and adapt it for Engine 2.0 later.

    Dylan Dalrymple: Thank you very much for you kind comments. Thanks! :D

comment Leave a Reply