Environmental Effects in 3D Graphics with XNA Game Studio 4.0

A step-by-step guide to adding the 3D graphics effects used by professionals to your XNA games.


3D Graphics with XNA Game Studio 4.0

3D Graphics with XNA Game Studio 4.0

A step-by-step guide to adding the 3D graphics effects used by professionals to your XNA games.

  • Improve the appearance of your games by implementing the same techniques used by professionals in the game industry
  • Learn the fundamentals of 3D graphics, including common 3D math and the graphics pipeline
  • Create an extensible system to draw 3D models and other effects, and learn the skills to create your own effects and animate them


        Read more about this book      

(For more resources on this subject, see here.)

We will look at a technique called region growing to add plants and trees to the terrain's surface, and finish by combining the terrain with our sky box, water, and billboarding effects to create a mountain scene:

Building a terrain from a heightmap

A heightmap is a 2D image that stores, in each pixel, the height of the corresponding point on a grid of vertices. The pixel values range from 0 to 1, so in practice we will multiply them by the maximum height of the terrain to get the final height of each vertex. We build a terrain out of vertices and indices as a large rectangular grid with the same number of vertices as the number of pixels in the heightmap.

Let's start by creating a new Terrain class. This class will keep track of everything needed to render our terrain: textures, the effect, vertex and index buffers, and so on.

public class Terrain
VertexPositionNormalTexture[] vertices; // Vertex array
VertexBuffer vertexBuffer; // Vertex buffer
int[] indices; // Index array
IndexBuffer indexBuffer; // Index buffer
float[,] heights; // Array of vertex heights
float height; // Maximum height of terrain
float cellSize; // Distance between vertices on x and z axes
int width, length; // Number of vertices on x and z axes
int nVertices, nIndices; // Number of vertices and indices
Effect effect; // Effect used for rendering
GraphicsDevice GraphicsDevice; // Graphics device to draw with
Texture2D heightMap; // Heightmap texture

The constructor will initialize many of these values:

public Terrain(Texture2D HeightMap, float CellSize, float Height,
GraphicsDevice GraphicsDevice, ContentManager Content)
this.heightMap = HeightMap;
this.width = HeightMap.Width;
this.length = HeightMap.Height;

this.cellSize = CellSize;
this.height = Height;

this.GraphicsDevice = GraphicsDevice;

effect = Content.Load<Effect>("TerrainEffect");

// 1 vertex per pixel
nVertices = width * length;

// (Width-1) * (Length-1) cells, 2 triangles per cell, 3 indices per
// triangle
nIndices = (width - 1) * (length - 1) * 6;

vertexBuffer = new VertexBuffer(GraphicsDevice,
typeof(VertexPositionNormalTexture), nVertices,

indexBuffer = new IndexBuffer(GraphicsDevice,
nIndices, BufferUsage.WriteOnly);

Before we can generate any normals or indices, we need to know the dimensions of our grid. We know that the width and length are simply the width and height of our heightmap, but we need to extract the height values from the heightmap. We do this with the getHeights() function:

private void getHeights()
// Extract pixel data
Color[] heightMapData = new Color[width * length];

// Create heights[,] array
heights = new float[width, length];

// For each pixel
for (int y = 0; y < length; y++)
for (int x = 0; x < width; x++)
// Get color value (0 - 255)
float amt = heightMapData[y * width + x].R;

// Scale to (0 - 1)
amt /= 255.0f;

// Multiply by max height to get final height
heights[x, y] = amt * height;

This will initialize the heights[,] array, which we can then use to build our vertices. When building vertices, we simply lay out a vertex for each pixel in the heightmap, spaced according to the cellSize variable. Note that this will create (width – 1) * (length – 1) "cells"—each with two triangles:

The function that does this is as shown:

private void createVertices()
vertices = new VertexPositionNormalTexture[nVertices];

// Calculate the position offset that will center the terrain at
(0, 0, 0)
Vector3 offsetToCenter = -new Vector3(((float)width / 2.0f) *
cellSize, 0, ((float)length / 2.0f) * cellSize);

// For each pixel in the image
for (int z = 0; z < length; z++)
for (int x = 0; x < width; x++)
// Find position based on grid coordinates and height in
// heightmap
Vector3 position = new Vector3(x * cellSize,
heights[x, z], z * cellSize) + offsetToCenter;

// UV coordinates range from (0, 0) at grid location (0, 0) to
// (1, 1) at grid location (width, length)
Vector2 uv = new Vector2((float)x / width, (float)z / length);

// Create the vertex
vertices[z * width + x] = new VertexPositionNormalTexture(
position, Vector3.Zero, uv);

When we create our terrain's index buffer, we need to lay out two triangles for each cell in the terrain. All we need to do is find the indices of the vertices at each corner of each cell, and create the triangles by specifying those indices in clockwise order for two triangles. For example, to create the triangles for the first cell in the preceding screenshot, we would specify the triangles as [0, 1, 4] and [4, 1, 5].

private void createIndices()
indices = new int[nIndices];

int i = 0;

// For each cell
for (int x = 0; x < width - 1; x++)
for (int z = 0; z < length - 1; z++)
// Find the indices of the corners
int upperLeft = z * width + x;
int upperRight = upperLeft + 1;
int lowerLeft = upperLeft + width;
int lowerRight = lowerLeft + 1;

// Specify upper triangle
indices[i++] = upperLeft;
indices[i++] = upperRight;
indices[i++] = lowerLeft;

// Specify lower triangle
indices[i++] = lowerLeft;
indices[i++] = upperRight;
indices[i++] = lowerRight;

The last thing we need to calculate for each vertex is the normals. Because we are creating the terrain from scratch, we will need to calculate all of the normals based only on the height data that we are given. This is actually much easier than it sounds: to calculate the normals we simply calculate the normal of each triangle of the terrain and add that normal to each vertex involved in the triangle. Once we have done this for each triangle, we simply normalize again, averaging the influences of each triangle connected to each vertex.

private void genNormals()
// For each triangle
for (int i = 0; i < nIndices; i += 3)
// Find the position of each corner of the triangle
Vector3 v1 = vertices[indices[i]].Position;
Vector3 v2 = vertices[indices[i + 1]].Position;
Vector3 v3 = vertices[indices[i + 2]].Position;

// Cross the vectors between the corners to get the normal
Vector3 normal = Vector3.Cross(v1 - v2, v1 - v3);

// Add the influence of the normal to each vertex in the
// triangle
vertices[indices[i]].Normal += normal;
vertices[indices[i + 1]].Normal += normal;
vertices[indices[i + 2]].Normal += normal;

// Average the influences of the triangles touching each
// vertex
for (int i = 0; i < nVertices; i++)

We'll finish off the constructor by calling these functions in order and then setting the vertices and indices that we created into their respective buffers:



Now that we've created the framework for this class, let's create the TerrainEffect.fx effect. This effect will, for the moment, be responsible for some simple directional lighting and texture mapping. We'll need a few effect parameters:

float4x4 View;
float4x4 Projection;

float3 LightDirection = float3(1, -1, 0);
float TextureTiling = 1;

texture2D BaseTexture;
sampler2D BaseTextureSampler = sampler_state {
Texture = <BaseTexture>;
AddressU = Wrap;
AddressV = Wrap;
MinFilter = Anisotropic;
MagFilter = Anisotropic;

The TextureTiling parameter will determine how many times our texture is repeated across the terrain's surface—simply stretching it across the terrain would look bad because it would need to be stretched to a very large size. "Tiling" it across the terrain will look much better.

We will need a very standard vertex shader:

struct VertexShaderInput
float4 Position : POSITION0;
float2 UV : TEXCOORD0;
float3 Normal : NORMAL0;

struct VertexShaderOutput
float4 Position : POSITION0;
float2 UV : TEXCOORD0;
float3 Normal : TEXCOORD1;

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
VertexShaderOutput output;

output.Position = mul(input.Position, mul(View, Projection));
output.Normal = input.Normal;
output.UV = input.UV;

return output;

The pixel shader is also very standard, except that we multiply the texture coordinates by the TextureTiling parameter. This works because the texture sampler's address mode is set to "wrap", and thus the sampler will simply wrap the texture coordinates past the edge of the texture, creating the tiling effect.

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
float light = dot(normalize(input.Normal),
light = clamp(light + 0.4f, 0, 1); // Simple ambient lighting

float3 tex = tex2D(BaseTextureSampler, input.UV * TextureTiling);
return float4(tex * light, 1);

The technique definition is the same as our other effects:

technique Technique1
pass Pass1
VertexShader = compile vs_2_0 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();

In order to use the effect with our terrain, we'll need to add a few more member variables to the Terrain class:

Texture2D baseTexture;
float textureTiling;
Vector3 lightDirection;

These values will be set from the constructor:

public Terrain(Texture2D HeightMap, float CellSize, float Height,
Texture2D BaseTexture, float TextureTiling, Vector3 LightDirection,
GraphicsDevice GraphicsDevice, ContentManager Content)
this.baseTexture = BaseTexture;
this.textureTiling = TextureTiling;
this.lightDirection = LightDirection;

// etc...

Finally, we can simply set these effect parameters along with the View and Projection parameters in the Draw() function:


Let's now add the terrain to our game. We'll need a new member variable in the Game1 class:

Terrain terrain;

We'll need to initialize it in the LoadContent() method:

terrain = new Terrain(Content.Load<Texture2D>("terrain"), 30, 4800,
Content.Load<Texture2D>("grass"), 6, new Vector3(1, -1, 0),
GraphicsDevice, Content);

Finally, we can draw it in the Draw() function:

terrain.Draw(camera.View, camera.Projection);


Our terrain looks pretty good as it is, but to make it more believable the texture applied to it needs to vary—snow and rocks at the peaks, for example. To do this, we will use a technique called multitexturing, which uses the red, blue, and green channels of a texture as a guide as to where to draw textures that correspond to those channels. For example, sand may correspond to red, snow to blue, and rock to green. Adding snow would then be as simple as painting blue onto the areas of this "texture map" that correspond with peaks on the heightmap. We will also have one extra texture that fills in the area where no colors have been painted onto the texture map—grass, for example.

To begin with, we will need to modify our texture parameters on our effect from one texture to five: the texture map, the base texture, and the three color channel mapped textures.

texture RTexture;
sampler RTextureSampler = sampler_state
texture = <RTexture>;
AddressU = Wrap;
AddressV = Wrap;
MinFilter = Anisotropic;
MagFilter = Anisotropic;

texture GTexture;
sampler GTextureSampler = sampler_state
texture = <GTexture>;
AddressU = Wrap;
AddressV = Wrap;
MinFilter = Anisotropic;
MagFilter = Anisotropic;

texture BTexture;
sampler BTextureSampler = sampler_state
texture = <BTexture>;
AddressU = Wrap;
AddressV = Wrap;
MinFilter = Anisotropic;
MagFilter = Anisotropic;

texture BaseTexture;
sampler BaseTextureSampler = sampler_state
texture = <BaseTexture>;
AddressU = Wrap;
AddressV = Wrap;
MinFilter = Anisotropic;
MagFilter = Anisotropic;

texture WeightMap;
sampler WeightMapSampler = sampler_state {
texture = <WeightMap>;
AddressU = Clamp;
AddressV = Clamp;
MinFilter = Linear;
MagFilter = Linear;

Second, we need to update our pixel shader to draw these textures onto the terrain:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
float light = dot(normalize(input.Normal), normalize(
light = clamp(light + 0.4f, 0, 1);

float3 rTex = tex2D(RTextureSampler, input.UV * TextureTiling);
float3 gTex = tex2D(GTextureSampler, input.UV * TextureTiling);
float3 bTex = tex2D(BTextureSampler, input.UV * TextureTiling);
float3 base = tex2D(BaseTextureSampler, input.UV * TextureTiling);

float3 weightMap = tex2D(WeightMapSampler, input.UV);

float3 output = clamp(1.0f - weightMap.r - weightMap.g -
weightMap.b, 0, 1);
output *= base;

output += weightMap.r * rTex + weightMap.g * gTex +
weightMap.b * bTex;

return float4(output * light, 1);

We'll need to add a way to set these values to the Terrain class:

public Texture2D RTexture, BTexture, GTexture, WeightMap;

All we need to do now is set these values to the effect in the Draw() function:


To use multitexturing in our game, we'll need to set these values in the Game1 class:

terrain.WeightMap = Content.Load<Texture2D>("weightMap");
terrain.RTexture = Content.Load<Texture2D>("sand");
terrain.GTexture = Content.Load<Texture2D>("rock");
terrain.BTexture = Content.Load<Texture2D>("snow");

        Read more about this book      

(For more resources on this subject, see here.)

Adding a detail texture to the terrain

Our last improvement to the terrain will be to add what is called a detail texture. This is essentially a noise texture that we blend in when the camera is close to the terrain to fake a higher resolution texture.

The terrain right now looks great from afar, but when the camera is close enough the texture will start to smudge and blur. However, if we increase the number of times the texture tiles, we start to see what is called strobing—where the high-resolution texture starts to flicker as it is scaled down in the distance. The easiest way to eliminate this effect is to just tile the main texture fewer times, but then we are left with a blurry texture up close as noted earlier. Adding a detail texture that fades in only when the camera is close to the terrain solves both of these problems. By multiplying the main texture(s) by the detail texture (which is tiled many more times across the terrain so that each tile is smaller and more detailed up close), we can make it look as though the main texture were higher resolution without getting the "strobe" effect at a distance.

To start with, we will need a few more effect parameters:

float DetailTextureTiling;
float DetailDistance = 2500;

texture DetailTexture;
sampler DetailSampler = sampler_state {
texture = <DetailTexture>;
AddressU = Wrap;
AddressV = Wrap;
MinFilter = Linear;
MagFilter = Linear;

We can then update our pixel's shader to blend in the detail texture and multiply the output with it. The lerp() function interpolates between solid white (1) and the detail texture based on the depth at the pixel we're shading.

float3 detail = tex2D(DetailSampler, input.UV * DetailTextureTiling);
float detailAmt = input.Depth / DetailDistance;
detail = lerp(detail, 1, clamp(detailAmt, 0, 1));

return float4(detail * output * light, 1);

We'll need to add more instance variables to the Terrain class to reflect these parameters:

public Texture2D DetailTexture;
public float DetailDistance = 2500;
public float DetailTextureTiling = 100;

We also need to set these effect parameters in the Draw() function:


Finally, we'll set the DetailTexture value in the game's LoadContent() method:

terrain.DetailTexture = Content.Load<Texture2D>("noise_texture");

Placing plants on the terrain

The next step in building our environment is to add some plants and trees to the terrain. We will look at two approaches to placing billboards on the terrain, and we will use both approaches to add vegetation to the terrain—one for trees and one for grass. First, we will need a function to find the height of the terrain at any given coordinate. This is a deceptively complex problem as we will need to interpolate between the heights at each vertex rather than just retrieving a rounded value from the heights array. The function to do this is as follows. Note that it also outputs the "steepness" of the terrain at the sampled point—this value is simply the angle between the lower and higher of the vertices at the edge of the cell being sampled:

// Returns the height and steepness of the terrain at point (X, Z)
public float GetHeightAtPosition(float X, float Z, out float Steepness)
// Clamp coordinates to locations on terrain
X = MathHelper.Clamp(X, (-width / 2) * cellSize,
(width / 2) * cellSize);
Z = MathHelper.Clamp(Z, (-length / 2) * cellSize,
(length / 2) * cellSize);

// Map from (-Width/2->Width/2,-Length/2->Length/2)
// to (0->Width, 0->Length)
X += (width / 2f) * cellSize;
Z += (length / 2f) * cellSize;

// Map to cell coordinates
X /= cellSize;
Z /= cellSize;

// Truncate coordinates to get coordinates of top left cell vertex
int x1 = (int)X;
int z1 = (int)Z;

// Try to get coordinates of bottom right cell vertex
int x2 = x1 + 1 == width ? x1 : x1 + 1;
int z2 = z1 + 1 == length ? z1 : z1 + 1;

// Get the heights at the two corners of the cell
float h1 = heights[x1, z1];
float h2 = heights[x2, z2];

// Determine steepness (angle between higher and lower vertex of
// cell)
Steepness = (float)Math.Atan(Math.Abs((h1 - h2)) / (cellSize *

// Find the average of the amounts lost from coordinates during
// truncation above
float leftOver = ((X - x1) + (Z - z1)) / 2f;

// Interpolate between the corner vertices' heights
return MathHelper.Lerp(h1, h2, leftOver);

We can now use this function to place our trees randomly on the terrain. We will need a random number generator and a BillboardSystem in our Game1 class:

Random r = new Random();
BillboardSystem trees;

When we get a random coordinate result from the random number generator, we will first check the height and steepness of the corresponding position on the terrain. If the steepness is more than 15 degrees or if the height does not fall into a reasonable range for trees to grow, we will reject the coordinates and try another random result. If we do find a good position for a tree, we simply add that position to the list to be drawn by the BillboardSystem. The following code, placed in the LoadContent function of the Game1 class, will do this for us:

// Positions where trees should be drawn
List<Vector3> treePositions = new List<Vector3>();
// Continue until we get 500 trees on the terrain
for (int i = 0; i < 500; i++) // 500
// Get X and Z coordinates from the random generator, between
// [-(terrain width) / 2 * (cell size), (terrain width) / 2 * (cell
float x = r.Next(-256 * 30, 256 * 30);
float z = r.Next(-256 * 30, 256 * 30);

// Get the height and steepness of this position on the terrain,
// taking the height of the billboard into account
float steepness;
float y = terrain.GetHeightAtPosition(x, z, out steepness) + 100;

// Reject this position if it is too low, high, or steep. Otherwise
// add it to the list
if (steepness < MathHelper.ToRadians(15) && y > 2300 && y < 3200)
treePositions.Add(new Vector3(x, y, z));
trees = new BillboardSystem(GraphicsDevice, Content,
Content.Load<Texture2D>("tree_billboard"), new Vector2(200),

trees.Mode = BillboardSystem.BillboardMode.Cylindrical;
trees.EnsureOcclusion = true;

Finally, we can draw the trees in the Draw() function:

trees.Draw(camera.View, camera.Projection, ((FreeCamera)camera).Up,

We will now use a second method to add grass to the terrain. Here, we will use a texture as a "map" to dictate where to place the terrain. We could use the previous technique equally well, but the purpose here is more to demonstrate this technique. We will add a second billboard system to draw the grass billboards, and place them according to our map, where a brighter pixel means a higher chance of placing a grass billboard:

We'll need a second BillboardSystem:

BillboardSystem grass;

We initialize the grass value as follows:

// List of positions to place grass billboards
List<Vector3> grassPositions = new List<Vector3>();

// Retrieve pixel grid from grass map
Texture2D grassMap = Content.Load<Texture2D>("grass_map");
Color[] grassPixels = new Color[grassMap.Width * grassMap.Height];

// Loop until 1000 billboards have been placed
for (int i = 0; i < 300; i++)
// Get X and Z coordinates from the random generator, between
// [-(terrain width) / 2 * (cell size), (terrain width) / 2 * (cell
float x = r.Next(-256 * 30, 256 * 30);
float z = r.Next(-256 * 30, 256 * 30);

// Get corresponding coordinates in grass map
int xCoord = (int)(x / 30) + 256;
int zCoord = (int)(z / 30) + 256;

// Get value between 0 and 1 from grass map
float texVal = grassPixels[zCoord * 512 + xCoord].R / 255f;

// Retrieve height
float steepness;
float y = terrain.GetHeightAtPosition(x, z, out steepness) + 50;

// Randomly place a billboard here based on pixel color in grass
// map
if ((int)((float)r.NextDouble() * texVal * 10) == 1)
grassPositions.Add(new Vector3(x, y, z));

// Create grass billboard system
grass = new BillboardSystem(GraphicsDevice, Content,
Content.Load<Texture2D>("grass_billboard"), new Vector2(100),

grass.Mode = BillboardSystem.BillboardMode.Cylindrical;
grass.EnsureOcclusion = false;

Again, we need to draw this billboard system in the Draw() function:

grass.Draw(camera.View, camera.Projection, ((FreeCamera)camera).Up,

        Read more about this book      

(For more resources on this subject, see here.)

Adding the finishing touches

Our scene is starting to look pretty good! Let's finish it off by first adding a billboard system to render clouds, and finally bring in our sky and water effects. First, we will add a third billboard system for our clouds.

BillboardSystem clouds;

Note that the following function creates more believable clouds by "clumping" billboards together instead of spreading them evenly across the sky.

List<Vector3> cloudPositions = new List<Vector3>();

// Create 20 "clusters" of clouds
for (int i = 0; i < 20; i++)
Vector3 cloudLoc = new Vector3(r.Next(-8000, 8000),
r.Next(4000, 6000), r.Next(-8000, 8000));

// Add 10 cloud billboards around each cluster point
for (int j = 0; j < 10; j++)
cloudPositions.Add(cloudLoc + new Vector3(r.Next(-3000, 3000),
r.Next(-300, 900), r.Next(-1500, 1500)));

clouds = new BillboardSystem(GraphicsDevice, Content,
Content.Load<Texture2D>("cloud2"), new Vector2(2000),

clouds.Mode = BillboardSystem.BillboardMode.Spherical;
clouds.EnsureOcclusion = false;

Once again, we'll need to draw the clouds in the Draw() function:

clouds.Draw(camera.View, camera.Projection, ((FreeCamera)camera).Up,

Finally, let's add in water and a sky:

SkySphere sky;
Water water;

We'll initialize them as follows:

sky = new SkySphere(Content, GraphicsDevice,

water = new Water(Content, GraphicsDevice, new Vector3(0, 1600, 0),
new Vector2(256 * 30));


Now, we can update the Draw() function to draw everything:

// Called when the game should draw itself
protected override void Draw(GameTime gameTime)
water.PreDraw(camera, gameTime);

sky.Draw(camera.View, camera.Projection, ((FreeCamera)camera).

foreach (CModel model in models)
if (camera.BoundingVolumeIsInView(model.BoundingSphere))
model.Draw(camera.View, camera.Projection,

terrain.Draw(camera.View, camera.Projection,

water.Draw(camera.View, camera.Projection,

trees.Draw(camera.View, camera.Projection, ((FreeCamera)camera).Up,

grass.Draw(camera.View, camera.Projection, ((FreeCamera)camera).Up,

clouds.Draw(camera.View, camera.Projection, ((FreeCamera)camera).Up,


There is one last thing we need to do—make the terrain IRenderable. We want the water to reflect the sky and the terrain, so we need to ensure that the Water class knows how to draw the terrain. We'll start by updating the Terrain class to include the IRenderable interface:

public class Terrain : IRenderable

We'll need to add a SetClipPlane function to this class as well:

public void SetClipPlane(Vector4? Plane)

if (Plane.HasValue)

Now, all we need to do is update the TerrainEffect.fx file to include clipping. First, we add the necessary effect parameters:

float4 ClipPlane;
bool ClipPlaneEnabled = false;

The VertexShaderOutput struct will now need to include the world space position:

float3 WorldPosition : TEXCOORD3;

We'll set this value in the vertex shader:

output.WorldPosition = input.Position;

Finally, we can perform the clipping at the beginning of the pixel shader:

if (ClipPlaneEnabled)
clip(dot(float4(input.WorldPosition, 1), ClipPlane));

Finally, we can add the terrain to the list of items the water should reflect (in the LoadContent() function of the Game1 class) and we're finished:



In this article, we learned a lot about environmental effects—terrain, clever placement of billboards, so-called "region growing" (placing billboards according to a texture)—and we also learned a lot about more advanced texturing techniques such as multitexturing and detail textures. We now have a very flexible terrain class, and a lovely environment to show it off in!

Further resources on this subject:

Books to Consider

comments powered by Disqus

An Introduction to 3D Printing

Explore the future of manufacturing and design  - read our guide to 3d printing for free