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.

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.

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.

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.

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:

// 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.

// 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.

// 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).

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.

Effect effect;

// In the constructor
effect = new BasicEffect(Engine.GraphicsDevice, null);
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:

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

Next remove the following from the Draw() method:

((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 float4x4 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.

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:

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:

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.

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).

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:

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.

string textureMap;
Texture2D texMap;

public string TextureMap
{
    get { return textureMap; }
    set { textureMap = value; texMap = Engine.Content.Load(value); }
}
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:

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#:

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

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#

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#

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:

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

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#

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

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:

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");
}

« »