Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Content Obsolete

This content is no longer current.

Beginning Game Development: Part VI - Lights, Materials and Terrain

  This is Part 6 of an introductory series on game programming using the Microsoft .NET Framework and managed DirectX 9.0. This article covers Lights and Materials and gives a very basic introduction to terrain.
3Leaf Development

Difficulty: Intermediate
Time Required: 3-6 hours
Cost: Free
Software: Visual Basic or Visual C# Express Editions, DirectX SDK
Hardware:
Download:
Beginning Game Development Series
  1. Beginning Game Development Part 1 - Introduction
  2. Beginning Game Development Part II - Introduction to DirectX
  3. Beginning Game Development: Part III - DirectX II
  4. Beginning Game Development: Part IV - DirectInput
  5. Beginning Game Development: Part V - Adding Units
  6. Beginning Game Development: Part VI - Lights, Materials and Terrain
  7. Beginning Game Development: Part VII –Terrain and Collision Detection
  8. Beginning Game Development: Part VIII - DirectSound
  9. Beginning Game Development: Part VIII - DirectSound II
  10. Beginning Game Development: Part VIII - DirectSound III

Introduction

Welcome to the sixth article on beginning game development. Last time I promised we would discuss lighting, terrain building and collision detecting in this article, but there is enough information in terrain building alone to cover multiple articles and the same holds true for collision detection. So instead, I am going to cover Lights and Materials and give a very basic introduction to terrain building in this article, and go into more depth about terrain building and collision detection in the next article.

Before we start, let's do the obligatory code cleanup, incorporating all the feedback I received.

Code cleanup

The cleanup for this article consists mainly of version upgrades and some minor performance improvements.

  • Updated to the release version of Visual Studio Express
  • Updated to the October 2005 DirectX SDK.
  • Added the following code to the ConfigureDevice method. Depth stencils enable the application to mask sections of the rendered image so they are not displayed, which increases performance. This code simply enables a 16-bit Z-Buffer depth stencil.
  • presentParams.AutoDepthStencilFormat = DepthFormat.D16;
  • presentParams.EnableAutoDepthStencil = true;
  • Added ClearFlags.ZBuffer to the Device.Clear method call in the Render method. This is to support the Depth Stencil added.
  • Added a speed parameter to the constructor of the Tank class.

Fonts

The only feedback provided to the game player so far has been the frame rate that was displayed in the title bar of the form. We also have written out some information to the console window, but that isn't very useful when running the game as an executable. In addition to the frame rate, I want to be able to display the location and heading. To do this we are going to start using DirectX fonts. The example here is the simplest case of drawing text to the screen; see the Text3D sample in the DirectX SDK for a more extensive explanation and detailed samples.

The first step is to declare the variable in the GameEngine class. I am using a fully qualified name, so there are no namespace collisions between the Font class in the Direct3D namespace and that in the System.Drawing namespace.

Visual C#

private Microsoft.DirectX.Direct3D.Font _font;

Visual Basic

Private m_font As Microsoft.DirectX.Direct3D.Font

Next initialize the Font class in the constructor of the GameEngine class.

Visual C#

font = new Microsoft.DirectX.Direct3D.Font(_device, 
   new System.Drawing.Font("Arial", 14.0f, FontStyle.Italic));

Visual Basic

m_font = New Microsoft.DirectX.Direct3D.Font(m_device, 
   New System.Drawing.Font("Arial", 14.0F, FontStyle.Italic))

The parameters should be self explanatory. Note that the DirectX Font class uses a Drawing Font class in its constructor.

All of the functionality to draw the various values to the screen is encapsulated in the RenderFonts method of the GameEngine class. This class uses the DrawText method of the Font class to actually render the text to the screen.

Visual C#

private void RenderFonts()
{
    // display the heading and pitch
    _font.DrawText(null, string.Format(
        "Heading={0:N000}, Pitch ={1:N000}", 
        _camera.Heading, _camera.Pitch), 
        new Rectangle(0, 0, this.Width, this.Height),
        DrawTextFormat.NoClip | DrawTextFormat.ExpandTabs |
        DrawTextFormat.WordBreak, Color.Yellow);

    // Display the Postion Direction
    _font.DrawText(null, string.Format("X={0}, Y={1}, Z={2}",   
        _camera.Position.X, _camera.Position.Y, _camera.Position.Z), 
        new Rectangle(0, 20, this.Width, this.Height),
        DrawTextFormat.NoClip | DrawTextFormat.ExpandTabs |  
        DrawTextFormat.WordBreak, Color.Yellow);

    // Display the frame rate
    _font.DrawText(null, string.Format("FPS={0}", 
        FrameRate.CalculateFrameRate()), 
        new Rectangle(0, 60,this.Width, this.Height),
        DrawTextFormat.NoClip | DrawTextFormat.ExpandTabs | 
        DrawTextFormat.WordBreak, Color.Yellow);

    // display Lighting state
    _font.DrawText(null, string.Format("Lights={0}", 
        _lightOn ? "On" : "Off"), 
        new Rectangle(this.Width - 110, 0,this.Width, this.Height),
        DrawTextFormat.NoClip | DrawTextFormat.ExpandTabs | 
        DrawTextFormat.WordBreak, Color.Yellow);
}

Visual Basic

Private Sub RenderFonts()

        ' display the heading and pitch
        m_font.DrawText(Nothing, String.Format(
            "Heading={0:N000}, Pitch ={1:N000}", 
            m_camera.Heading, m_camera.Pitch), 
            New Rectangle(0, 0, Me.Width, Me.Height), 
            DrawTextFormat.NoClip Or DrawTextFormat.ExpandTabs Or 
            DrawTextFormat.WordBreak, Color.Yellow)

        ' Display the Postion Direction
        m_font.DrawText(Nothing, String.Format(
            "X={0}, Y={1}, Z={2}", m_camera.Position.X,
            m_camera.Position.Y, m_camera.Position.Z), 
            New Rectangle(0, 20, Me.Width, Me.Height),
            DrawTextFormat.NoClip Or DrawTextFormat.ExpandTabs Or
            DrawTextFormat.WordBreak, Color.Yellow)

        ' Display the frame rate
        m_font.DrawText(Nothing, String.Format(
            "FPS={0}", FrameRate.CalculateFrameRate()), 
            New Rectangle(0, 60, Me.Width, Me.Height),
            DrawTextFormat.NoClip Or DrawTextFormat.ExpandTabs Or
            DrawTextFormat.WordBreak, Color.Yellow)

        ' display Lighting state
        If m_lightOn = True Then
            m_font.DrawText(Nothing, "Lights=On", 
                New Rectangle(Me.Width - 110, 0, Me.Width, Me.Height),
                DrawTextFormat.NoClip Or DrawTextFormat.ExpandTabs Or
                DrawTextFormat.WordBreak, Color.Yellow)
        Else
            m_font.DrawText(Nothing, "Lights=Off", 
                New Rectangle(Me.Width - 110, 0, Me.Width, Me.Height),
                DrawTextFormat.NoClip Or DrawTextFormat.ExpandTabs Or
                DrawTextFormat.WordBreak, Color.Yellow)
        End If

    End Sub

The first two calls to the DrawText method of the GameEngine class add the heading and pitch information and position of the camera. These two pieces of information now allow me to properly orient myself in the 3D world. The last two items add the frame rate counter (I removed the one in the title of the form) and a lighting state indicator that is useful when experimenting with lighting.

The location of the text on the screen is determined by the Rectangle class. The first two values determine where the upper left hand corner of the rectangle is (in screen coordinates), while the last two determine the size of the rectangle.

Lighting

Up to this point we have turned lighting off by setting the Lighting property of the RenderState to false. This means that every object vertex is drawn solely based on its defined color. Turning lighting on adjusts the color of each vertex by combining:

  • Its current Material color
  • Texels in associated texture maps
  • Diffuse and Specular colors
  • Color and intensity of all lights in the scene, including the ambient light

DirectX breaks light into two groups:

  1. Ambient: Ambient light is a special type of light that has been scattered so much that it no longer has a direction or source. Ambient light illuminates equally in every direction. For ambient light you can only define color and intensity. Ambient light does not contribute to specular reflection and the level of ambient light is independent of any other lights in the scene. Ambient light is the least expensive (in terms of computational needs) of all the lights. Without ambient light, objects in the shadows would be completely black. In DirectX ambient light is implemented in the Device RenderState.

  2. Directional: As is implied in the name, directional light has a specified direction in addition to a color and intensity. When direct light is reflected it does not contribute to the ambient light level of the scene, but it is used to compute specular highlights. Directional lights are represented by three types of lights which are added to the Lights array of the Device. In DirectX, direction is the distance from the logical origin, regardless of the position of the light in the scene. A direction vector of (0,0,1) points straight into the scene and a direction vector of (0,-1,0) points straight down. You can also create angles by mixing and matching the values in the direction vector.
    • Directional. This is a light source that has no position and produces light that travels parallel in one direction. In games, the sun and the moon are most often modeled as directional lights. Directional lights are relatively inexpensive but should be used in moderation, as adding many of them will negatively impact your frame rate.

    • Point. A Point light has a position and radiates light equally in all directions. Examples of point lights are bare light bulbs and torches. Point lights are more expensive than directional lights. Unlike Directional lights, a point light has an attenuation (how the light level decreases over distance) and a range (the maximum distance the light will travel).

    • Spot. A spot light is like a flashlight or car headlight. It is the most complex and expensive of all the light types. A spot light has a position and a direction. The light is separated by intensity into two cones: in the inner cone the light shines more brightly than in the outer cone. Only objects that fall within the cone (theta) are illuminated. In addition to defining the position, direction, range and attenuation of the light, you also must define the cone size and amount of falloff between the cones.

Materials

In the last article we briefly touched on materials. Materials define how lights reflect off a surface. For each material in DirectX you can set properties that define how it reflects ambient, diffuse and specular light.

Normal: For all this lighting stuff to work, DirectX has to know the vector normal for each face of the object (a cube would have 6 faces). A normal is nothing other than a vector that points away from the face at a 90 degree angle. (Check out the managed SDK documentation, as it has an excellent description of a normal. Go to Introducing DirectX 9.0 > Direct3D Graphics > Getting Started with Direct3D > 3-D coordinate Systems and Geometry > Face and Vertex Normal Vectors.)

Color

DirectX describes color in terms of four components: Red, Green, Blue and Alpha (RGBA). The values of these components can range from 0.0f to 1.0f. While both materials and lights use the same color structure, they use color differently.

For lights, the color values are the amount of light emitted by that color component. A value of 0.0f means that the component is turned off and a value of 1.0 means that it is at max brightness. The Alpha value is not used. You can also set the value to a number higher than 1.0f to create a very bright light or to a negative number to create a light that actually removes light from a scene.

For materials, the color values are the amount of light that is reflected by a surface. A value of 0.0f means that no light for that component is reflected and a value of 1.0f means that all light is reflected for that component.

Color Types

Each type of light can emit four colors. The color of the light interacts with the counterpart in the material to produce the final color used to render the object: e.g. the diffuse color of the light interacts only with the diffuse property of the material.

  1. Ambient: Ambient light is the general background light and is the same everywhere in the scene.
  2. Diffuse: Diffuse light is scattered but still maintains an overall direction. A diffuse surface reflects incoming lights across all angles; this causes the material to look dull or matte.
  3. Specular: This is the opposite of Diffuse. Specular light is not scattered at all and makes a surface appear reflective. To use specular lighting you must first enable it at the Device Render State.
  4. Emissive: DirectX also has the principle of Emissive light. This is light that is emitted by objects. Emissive light is not cast onto other objects.

Now enough with the theory, and on to the code.

Adding Lights

The first step is to change the RenderState.Lighting property to true. You can also remove the line entirely, since true is the default value. If you forget this step, then no lights will show regardless of how many lights are enabled. We do this in the ConfigureDevice method, since this is a global setting.

At the bottom of the ConfigureDevice method, add the following code:

Visual C#

_device.RenderState.Lighting = true;

Visual Basic

m_device.RenderState.Lighting = True

Each scene can only contain a single ambient light, but multiple lights of the other three types. For this reason the Ambient light is implemented at the RenderState level while the other lights are stored in the Lights array of the Device class. For BattleTank2005 we add some ambient white light.

At the bottom of the ConfigureDevice method, add the following code immediately after the previous added line of code:

Visual C#

_device.RenderState.Ambient = Color.White;

Visual Basic

m_device.RenderState.Ambient = Color.White

Most modern graphics cards support advanced lighting techniques, such as directional lights, and up to 8 simultaneous active lights (but you can define as many lights as you want and then turn them on/off depending on the scene). The DeviceCaps.MaxActiveLights property of the Device determines the exact number. If the value is zero, you can not use any lights and must default to ambient lighting only. You can also query the graphics card to see how many other lights it supports, and then adjust your lighting strategy accordingly.

Add a method called CreateLights to the GameEngine class with the following code:

Visual C#

if ( _device.DeviceCaps.MaxActiveLights == 0 )
{
    _device.RenderState.Ambient= Color.White;
}

Visual Basic

If m_device.DeviceCaps.MaxActiveLights = 0 Then
    m_device.RenderState.Ambient = Color.White
End If

For BattleTank2005 we are going to add just one directional light to simulate the sun. In the CreateLights method add the following code.

Visual C#

else
{
    if ( _device.DeviceCaps.MaxActiveLights > 1 )
    {
        // This directional Light is our "sun"
        _device.Lights[0].Type = LightType.Directional;
        // Point the light straight down
        _device.Lights[0].Direction = new Vector3( 0f, -1.0f, 0f);
        _device.Lights[0].Diffuse = System.Drawing.Color.LightYellow;
        _device.Lights[0].Enabled = true;
    }
}

Visual Basic

Else
    If m_device.DeviceCaps.MaxActiveLights > 1 Then
        ' This directional Light is our "sun"
        m_device.Lights(0).Type = LightType.Directional
        ' Point the light down
        m_device.Lights(0).Direction = New Vector3(0.0F, -1.0F, 0.0F)
        m_device.Lights(0).Diffuse = System.Drawing.Color.White
        m_device.Lights(0).Enabled = True
    End If
End If

The last step is to call this method. Add the following code to the constructor of the GameEngine class immediately after the call to CreateTanks.

CreateLights ( );

One other change to make is to turn off the lights when rendering the skybox so it remains unaffected by any light settings. In the Render method of the Skybox class, add the following code immediately after the code disabling the Z-Buffer.

Visual C#

_device.RenderState.Lighting = false;

Visual Basic

m_device.RenderState.Lighting = False

After rendering the skybox we need to make sure to turn the lights back on. Add the following code immediately after the code enabling the Z-buffer.

Visual C#

_device.RenderState.Lighting = true;

Visual Basic

m_device.RenderState.Lighting = True

That's all we need to add lighting. The best way to really understand lighting and materials is seeing them in action. Go ahead and add some of the other light types to the game, or change the materials and see what happens when you manipulate the RGBA values.

Adding Terrain

Have you ever noticed how most games are set in space or indoors? The reason is that creating realistic looking outdoor terrain, without bringing the game to a grinding halt, is very difficult.

Terrain creation is an extensive subject to cover. Some developers specialize in nothing else, so have developed incredibly refined algorithms and methods to display the most realistic terrain, using the least amount of resources. While it is not possible to cover all the methods available, I do want to try to give you an understanding of the basics so that you can enhance Battletank2005 with more a refined technique of your choosing.

Height Map

A terrain starts out as a regular grid mesh. In a regular grid mesh, all the points are equally distant from one another. Each point consists of its X,Z location and a Y value to express the height of the terrain at that point. If we were to create a simple 3x3 terrain the grid would look like this.

This simple 3x3 terrain consists of 18 triangles (or 9 quads) and it takes 36 vertices to define the points required to draw the 18 triangles. These numbers are important to understand because we will use them extensively in creating the terrain.

The easiest way to store the height data is in a height map represented by a grayscale image. Each pixel in the image represents one point in the grid with the height information represented by the gray scale values. Darker colors are lower elevations and lighter colors are higher elevations. Since the number of shades of gray is 256 (0-255) we can represent 256 distinct height values.

Sample Height map

Most applications use the RAW format, which is basically a linear array of bytes. You can think of a RAW file as an image file with the header and footer information stripped out. Loading the height data from a RAW file is much faster than loading it from an image file. I have included methods for both in the code, so you can experiment with them and see for yourself. The big advantage in using an image file over a RAW file is that you can see the heightmap. You can also export image files to a RAW file using a number of free conversion utilities available if you use a program like HME or Terragen to create the height map.

In addition to creating the heightmap manually, you can use algorithms such as Fault Formation or Midpoint Displacement to create them programmatically. This approach would be useful if you include a terrain generator with your game or if you want to support a large number of random terrain setups.

Regardless how you choose to create or load your height information, the entire process of starting with a height map and ending up with a realistic-looking 3D terrain involves the following steps:

  1. Load the height information from the height map into an array.
  2. Store the vertices for the regular grid mesh in a vertex buffer.
  3. Store the indexes for the regular grid mesh in an index buffer.
  4. Compute the normals for each triangle.
  5. Render the vertices as a triangle strip.

Terrain Class

To represent the terrain in BattleTank2005, I added a terrain class. This class will encapsulate all terrain-related logic. The terrain class will be initialized in the constructor and rendered in the regular render loop. To render the terrain we are going to use a single TriangleStrip, a Vertex Buffer and Index Buffer.

The first step is to actually load the height data into memory. The code also contains the method for loading this data from an image, but the preferred method is to load the data from a RAW file.

Visual C#

public void LoadHeightMapFromRAW ( string fileName )
{
    _isHeightMapRAW = true;
    _elevations = null;
  
    using ( Stream stream = File.OpenRead ( fileName ) )
    {
        _elevationsRAW = new byte[(int)stream.Length];
        stream.Read ( _elevationsRAW, 0, (int)stream.Length );
        ComputeValues ( (int)Math.Sqrt( (double)stream.Length ), 
                        (int)Math.Sqrt( (double)stream.Length ));
    }

    // Now load the buffers
    LoadVertexBuffer ( );
            
    LoadIndexBuffer ( );
}

Visual Basic

Public Sub LoadHeightMapFromRAW(ByVal fileName As String)
        m_isHeightMapRAW = True
        _elevations = Nothing

        Dim stream As New FileStream(fileName, FileMode.Open)
        _elevationsRAW = New Byte(stream.Length) {}
        stream.Read(_elevationsRAW, 0, CType(stream.Length, Integer))

        ComputeValues(CType(Math.Sqrt(CType(stream.Length, Double)),
        Integer), CType(Math.Sqrt(CType(stream.Length, Double)), Integer))

        ' Now load the buffers
        LoadVertexBuffer()
        LoadIndexBuffer()
End Sub

The first two lines are only present to support loading height data from both format types. The meat of the method is the stream.Read method. This method copies the content of the stream buffer into the elevation buffer byte array. Accessing the stream inside of the using statement ensures that the stream is properly closed and disposed.

Once the data has been loaded, we use the length of the stream to compute the various values that we will need for terrain generation.

Visual C#

private void ComputeValues ( int width, int height )
{
    // Vertices
    _numberOfVerticesX = width;
    _numberOfVerticesZ = height;
    _totalNumberOfVertices = _numberOfVerticesX * _numberOfVerticesZ;

    // Quads
    _numberOfQuadsX = _numberOfVerticesX - 1;
    _numberOfQuadsZ = _numberOfVerticesZ - 1;
    _totalNumberOfQuads  = _numberOfQuadsX * _numberOfQuadsZ;

    _totalNumberOfTriangles = _totalNumberOfQuads * 2;
    _totalNumberOfIndicies = _totalNumberOfQuads * 6;
}

Visual Basic

 Private Sub ComputeValues(ByVal width As Integer, 
    ByVal height As Integer)

        ' Vertices
        _numberOfVerticesX = width
        _numberOfVerticesZ = height
        _totalNumberOfVertices = _numberOfVerticesX * _numberOfVerticesZ

        ' Quads
        _numberOfQuadsX = _numberOfVerticesX - 1
        _numberOfQuadsZ = _numberOfVerticesZ - 1
        _totalNumberOfQuads = _numberOfQuadsX * _numberOfQuadsZ

        _totalNumberOfTriangles = _totalNumberOfQuads * 2
        _totalNumberOfIndicies = _totalNumberOfQuads * 6

    End Sub

Once the height data has been loaded and the various dimensions computed, we can populate the vertex buffer.

Visual C#

   private void LoadVertexBuffer ( )
{
    // This is the buffer we are going to store the vertices in
    _vb = new VertexBuffer ( typeof(CustomVertex.PositionNormalTextured),  
        _totalNumberOfVertices, _device, Usage.WriteOnly, 
        CustomVertex.PositionNormalTextured.Format, Pool.Managed );
            
    // All the vertices are stored in a 1D array
    _vertices = new CustomVertex.PositionNormalTextured[
       _totalNumberOfVertices];
            
    // Load vertices into the buffer one by one
    for ( int z = 0; z < _numberOfVerticesZ; z++ )
    {
        for ( int x = 0; x < _numberOfVerticesX; x++ )
        {
            CustomVertex.PositionNormalTextured vertex;
            vertex.X = x;
            vertex.Z = z;
            
            // Set the Y to the elevation value in the elevation array
            if ( _isHeightMapRAW )
            vertex.Y = (float)_elevationsRAW[ 
               ( z * _numberOfVerticesZ ) + x];
            else
            vertex.Y = _elevations[x,z];
            
            // Set the u,v values so one texture covers the entire terrain
            vertex.Tu = (float)x / _numberOfQuadsX;
            vertex.Tv = (float)z / _numberOfQuadsZ;
            
            // Set up a bogus normal
            vertex.Nx = 0;
            vertex.Ny = 1;
            vertex.Nz = 0;
            
            // Add it to the array
            // Note: this is the same formula used in the elevations 
            //computation
            // to map the 2D array coordaintes into a 1D array
            _vertices[ (  z * _numberOfVerticesZ ) + x ] = vertex;
        }
    }
    
    // No overide the bogus normal computations with a real one
    ComputeNormals ( );
    
    // finally set assign the vertices array to the buffer
    _vb.SetData ( _vertices, 0, LockFlags.None );
}

Visual Basic

Private Sub LoadIndexBuffer()
    Dim numIndices As Integer = (_numberOfVerticesX * 2) *     
                (_numberOfQuadsZ) + _numberOfVerticesZ - 2
    _indices = New Integer(numIndices) {}
    _ib = New IndexBuffer(GetType(Integer), _indices.Length, _device,
        Usage.WriteOnly, Pool.Managed)
    Dim index As Integer = 0
    Dim z As Integer = 0
    While z < _numberOfQuadsZ
        If z Mod 2 = 0 Then
            Dim x As Integer
            x = 0
            x = 0
            While x < _numberOfVerticesX
              _indices(System.Math.Min(
                  System.Threading.Interlocked.Increment(index), 
                  index - 1)) = x + (z * _numberOfVerticesX)
              _indices(System.Math.Min(
                  System.Threading.Interlocked.Increment(index), 
                  index - 1)) = x + (z * _numberOfVerticesX) + 
                  _numberOfVerticesX
                                                    
              System.Math.Min(System.Threading.Interlocked.Increment(x), 
                  x - 1)
            End While
            If Not (z = _numberOfVerticesZ - 2) Then
               _indices(System.Math.Min(
                   System.Threading.Interlocked.Increment(index), 
                   index - 1)) = System.Threading.Interlocked.Decrement(x)
                   + (z * _numberOfVerticesX)
            End If
        Else
            Dim x As Integer
            x = _numberOfVerticesX - 1
            x = _numberOfVerticesX - 1
            While x >= 0
                _indices(System.Math.Min(
                    System.Threading.Interlocked.Increment(index), 
                    index - 1)) = x + (z * _numberOfVerticesX)
                _indices(System.Math.Min(
                    System.Threading.Interlocked.Increment(index), 
                    index - 1)) = x + (z * _numberOfVerticesX) 
                    + _numberOfVerticesX
                System.Math.Max(System.Threading.Interlocked.Decrement(x), 
                    x + 1)
            End While
            If Not (z = _numberOfVerticesZ - 2) Then
                _indices(System.Math.Min(
                    System.Threading.Interlocked.Increment(index),
                    index - 1)) = 
                    System.Threading.Interlocked.Increment(x) + 
                    (z * _numberOfVerticesX)
            End If
        End If
        System.Math.Min(System.Threading.Interlocked.Increment(z), z - 1)
    End While
    _ib.SetData(_indices, 0, 0)
End Sub

This method loops over the dimensions of the terrain and creates a vertex for each point, consisting of the X, Z coordinates and the height information (Y) for each point. At this stage we do not actually compute the normal, since we are going to average the vector values of the neighboring vertices later on and need the entire buffer to do so.

The Tu and Tv values determine how the Texture for the landscape is applied to the vertex. You can think of the Tu and Tv values as the X and Y values respectively of the texture. The values are floating-point values in the range of 0.0 to 1.0. A pair of u.v coordinates is called a Texel. We will cover textures and terrain in more detail in the next article. Right now we are just covering the terrain using a single terrain texture. (Actually, we are using the bottom of the skybox to do so; this ensures that the terrain matches the skybox closely.)

Now that we have the entire vertex buffer, we can go back and compute the normal for each vertex.

Visual C#

private void ComputeNormals ( )
{
    // compute normals
    for ( int z = 1; z < _numberOfQuadsZ; z ++)
    {
        for ( int x = 1; x < _numberOfQuadsX; x 

++)
        {
            // Use the adjoing 

vertices along both axis to compute the new 
            //normal
            Vector3 X = Vector3.Subtract ( 
                _vertices[ z * _numberOfVerticesZ + x + 1 ].Position, 
                _vertices[  z *_numberOfVerticesZ + x - 1].Position );
            Vector3 Z = Vector3.Subtract ( 
                _vertices[ (z+1) * _numberOfVerticesZ + x ].Position, 
                _vertices[(z-1)*_numberOfVerticesZ+x].Position );
            Vector3 Normal = Vector3.Cross ( Z, X );
            Normal.Normalize();
            _vertices[ ( z *_numberOfVerticesZ ) + x].Normal = Normal;
        }
    }
}

Visual Basic

Private Sub ComputeNormals()
    ' compute normals
    Dim z As Integer = 1
    While z < _numberOfQuadsZ
        Dim x As Integer = 1
        While x < _numberOfQuadsX
            ' Use the adjoing vertices along both axis to 
            ' compute the new normal
            Dim VX As Vector3 = Vector3.Subtract(
                _vertices(z * _numberOfVerticesZ + x + 1).Position,
                _vertices(z * _numberOfVerticesZ + x - 1).Position)
            Dim VZ As Vector3 = Vector3.Subtract(_vertices((z + 1) *
                _numberOfVerticesZ + x).Position, _vertices((z - 1) *
                _numberOfVerticesZ + x).Position)
            Dim Normal As Vector3 = Vector3.Cross(VZ, VX)
            Normal.Normalize()
            _vertices((z * _numberOfVerticesZ) + x).Normal = Normal
            x = x + 1
        End While
        z = z + 1
    End While

We simply use the two neighboring values along the X and Z axes to compute an average normal for the vertex.

Note   If you are just dying to go into detail for normal computations, read this paper: http://www.gamedev.net/reference/articles/article2264.asp.

After creating the vertex buffer, we need to create the index buffer. Index buffers are a DirectX mechanism for sharing vertex data by storing the indices into vertex buffers. Basically, they are a way to store information more efficiently. (Go to Introducing DirectX 9.0 > Direct3D Graphics > Getting Started with Direct3D > Direct3D Rendering > Rendering Primitives > Rendering from Vertex and Index Buffers in the DirecxtX managed SDK for an excellent discussion of vertex and index buffers.)

In terrain creation, the large number of vertices makes the use of index buffers essential for good performance.

Visual C#

private void LoadIndexBuffer ( )
{
    int numIndices = (_numberOfVerticesX * 2) * (_numberOfQuadsZ) + 
                     (_numberOfVerticesZ - 2);
    
    _indices = new int[numIndices];

    _ib = new IndexBuffer ( typeof( int ), _indices.Length, 
        _device, Usage.WriteOnly, Pool.Managed );

    int index = 0;

    for ( int z = 0; z < _numberOfQuadsZ; z++ )
    {
        if ( z % 2 == 0 )
        {
            int x;
            for ( x = 0; x < _numberOfVerticesX; x++ )
            {
                _indices[index++] = x + (z * _numberOfVerticesX);
                _indices[index++] = x + (z * _numberOfVerticesX) + 
                                    _numberOfVerticesX;
            }
            if ( z != _numberOfVerticesZ - 2)
            {
                _indices[index++] = --x + (z * _numberOfVerticesX);
            }
        }
        else
        {
            int x;
            for ( x = _numberOfVerticesX - 1; x >= 0; x-- )
            {
                _indices[index++] = x + (z * _numberOfVerticesX);
                _indices[index++] = x + (z * _numberOfVerticesX) + 
                                    _numberOfVerticesX;
            }
            if ( z != _numberOfVerticesZ - 2)
            {
                _indices[index++] = ++x + (z * _numberOfVerticesX);
            }
        }
    } 
    _ib.SetData( _indices, 0, 0 );
}

Visual Basic

Private Sub LoadIndexBuffer()
    Dim numIndices As Integer = (_numberOfVerticesX * 2) * 
    (_numberOfQuadsZ) + _numberOfVerticesZ - 2
    _indices = New Integer(numIndices) {}
    _ib = New IndexBuffer(GetType(Integer), 
        _indices.Length, 
        _device, Usage.WriteOnly, 
        Pool.Managed)

    Dim index As Integer = 0
    Dim z As Integer = 0
    While z < _numberOfQuadsZ
        If z Mod 2 = 0 Then
            Dim x As Integer
            x = 0
            x = 0
            While x < _numberOfVerticesX
                _indices(System.Math.Min(
                    System.Threading.Interlocked.Increment(index), 
                    index - 1)) = x + (z * _numberOfVerticesX)
                _indices(System.Math.Min(
                    System.Threading.Interlocked.Increment(index), 
                    index - 1)) = x + (z * _numberOfVerticesX) 
                    + _numberOfVerticesX

                System.Math.Min(System.Threading.Interlocked.Increment(x),
                    x - 1)
            End While
            If Not (z = _numberOfVerticesZ - 2) Then
                _indices(System.Math.Min(
                System.Threading.Interlocked.Increment(index), 
                index - 1)) = System.Threading.Interlocked.Decrement(x) 
                + (z * _numberOfVerticesX)
            End If
        Else
            Dim x As Integer
            x = _numberOfVerticesX - 1
            x = _numberOfVerticesX - 1
            While x >= 0
                _indices(System.Math.Min(
                   System.Threading.Interlocked.Increment(index), 
                   index - 1)) = x + (z * _numberOfVerticesX)
                _indices(System.Math.Min(
                   System.Threading.Interlocked.Increment(index), 
                   index - 1)) = x + (z * _numberOfVerticesX) 
                   + _numberOfVerticesX
                System.Math.Max(System.Threading.Interlocked.Decrement(x),
                    x + 1)
            End While
            If Not (z = _numberOfVerticesZ - 2) Then
                _indices(System.Math.Min(
                    System.Threading.Interlocked.Increment(index),
                    index - 1)) =System.Threading.Interlocked.Increment(x) 
                    + (z * _numberOfVerticesX)
            End If
        End If
        System.Math.Min(System.Threading.Interlocked.Increment(z), z - 1)
    End While
    _ib.SetData(_indices, 0, 0)

End Sub

Building the index buffer is probably the most difficult to understand, and there are multiple ways of doing this. The method shown here builds a single TriangleStrip in a snake-like motion from the bottom to the top. Even rows are built left-to-right and odd rows right-to-left.

The tricky part is moving from the last triangle in the row to the next row up. If we go straight up and the next vertex is not in line, then we end up with an extra triangle at the end. To suppress this extra triangle, we render a degenerate triangle (a triangle with no volume) by simply repeating the last vertex in each row. Since each adjoining triangle has the opposite winding, we have to repeat the vertex once more, otherwise the triangle would be considered back-facing and not rendered.

If you don't understand this at first, don't worry; neither did I. Play with the VertexBuffer and IndexBuffer creation methods using a small grid (like 3x3) to see the vertices and plot them one by one on paper creating the TriangleStrip by hand.

The last step is to provide a way to render the terrain.

Visual C#

public void Render ( )
{
    _device.Material = _material;

    // Adjust the unit to the selected scale
    _device.Transform.World = Matrix.Scaling ( 1.0f, 0.3f, 1.0f );
    _device.SetTexture ( 0, _terrainTexture );
    _device.Indices = _ib;
    _device.SetStreamSource ( 0, _vb, 0 );
    _device.VertexFormat = CustomVertex.PositionNormalTextured.Format;
    _device.DrawIndexedPrimitives ( PrimitiveType.TriangleStrip, 0, 0,
        _totalNumberOfVertices, 0, _indices.Length - 2 );
}

Visual Basic

Public Sub Render()

    _device.Material = _material

    ' Adjust the unit to the selected scale
    _device.Transform.World = Matrix.Scaling(1.0F, 0.3F, 1.0F)
    _device.SetTexture(0, _terrainTexture)
    _device.Indices = _ib
    _device.SetStreamSource(0, _vb, 0)
    _device.VertexFormat = CustomVertex.PositionNormalTextured.Format
    _device.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0,
        _totalNumberOfVertices, 0, _indices.Length - 2)

End Sub

This code should look very familiar from the earlier articles. The main difference between this render call and previous ones is the use of the index buffer and the corresponding DrawIndexedPrimitives call. We are also using the scaling matrix to make the terrain less hilly by scaling it down on the Y axis. You can also scale it up on the Z and X axis to get a larger terrain without having to load in a larger height map.

To integrate the terrain class into the game, we need to add a variable to the GameEngine class.

Visual C#

private Terrain 

_terrain;

Visual Basic

Private m_terrain 

As Terrain

Then we initialize the Terrain class and call the appropriate LoadHeightMap method in the constructor of the GameEngine class.

Visual C#

_terrain = new Terrain ( "Down.jpg", this._device );
_terrain.LoadHeightMapFromRAW ( " Heightmap256.raw" );

Visual Basic

m_terrain = New Terrain("Down.jpg", m_device)
m_terrain.LoadHeightMapFromRAW("Heightmap256.raw")

And finally we add the Terrain class to the Render loop.

Visual C#

_terrain.Render (  );

Visual Basic

_terrain.Render (  );

The finished result should look something like the picture below.

Click here for larger image

(click image to zoom)

In this particular picture I am rendering in wire-frame mode, so the skybox is rendered as a simple wire-frame cube, the corner of which is visible in the bottom of this screenshot. Zooming in closer, you can clearly see the regular grid mesh with Y values.

Click here for larger image

(click image to zoom)

For this version I have added support for switching among the various render modes. Press F1 to see the scene rendered in wire-frame mode, F2 to see it rendered in solid mode, and F3 to see it rendered in Point mode. I also set up the F4 and F5 keys to toggle the directional light on and off, so you can easily see the difference.

Summary

Wow! That was a lot to cover and I haven't even started talking about automatic texture mapping, height-based lighting, light-maps, Level Of Detail, ROAM, Geomipmapping, quadtrees and culling. I also still need to cover collision detection, and finally, I want to adjust our camera pitch, yaw and roll to conform to the underlying terrain, so the game provides a realistic "driving" experience. As usual, I hope you experiment with the code to gain a good understanding of the issues we just covered. Next time we will address more advanced terrain and cover creating and rendering issues as well as collision detection.

Until then: Happy coding.

Follow the Discussion

  • Eric DzikowskiEric Dzikowski

    Just a note to let you know that I think this series is excellent.  It shocks me that you have no comments on any of these articles.  You've done a great job and I hope you continue with this series.

  • HamedHamed

    im mulimedia & Graphic Programmer And

    I Want professional sample From Directx By C#

  • ShadowShadow

    I have been testing in my pc this guide, and everything seems to be ok, except that the terrain is not being rendered in the screen.

    I dont know why, there is no error.

    I'm using

    WinXP SP2

    .Net framework 2 and 3 installed

    VS.NET 2005 Standar Edition

    December 2006 DirectX SDK

    It just dont render the triangles.

    I was wondering i anyone else has have this problem. or its just me.

    Any hekp will be appreciated.

    Thank you very much

    faranda@runagames.com

  • Abi George UllattilAbi George Ullattil

    I think there's a typo in the LoadVertexBuffer() section... the VB corresponds to the LoadIndexBuffer()... and also for some reason i'm getting an error when trying the LoadIndexBuffer function.... i'm doing this in C++.

    cheers!!!

  • Jeromy WalshJeromy Walsh

    I just wanted to thank you for referencing my article about normal computations for heightfield lighting on GameDev.net.  It's good to know that it is still providing further use to people.

  • GooseGoose

    @Shadow:

    I'm getting the same thing. However, when you rotate the camera around a bit, sometimes you see lots of triangles being drawn all over each other - and this scene changes every frame whether you move or not.

    Not sure where the problem is - any help would be GREATLY appreciated.

  • Bermil RossiterBermil Rossiter

    bmr12@sbcglobal.net

    Windows Vista Home Premium

    Microsoft DirectX SDK (August 2007)

    Microsoft Visual C# 2005 Express Edition - ENU

    Microsoft SDKs\Windows\v6.0

    In Parts VI, VII, and VIII I get a run time error from the folloeing code

    The last line of   Render() in class Terrain

    _device.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, _totalNumberOfVertices, 0, _indices.Length - 2);

    Help!!!!

  • TankCreatorTankCreator

    I got stuck on this one for a few days now, but I managed to get it working. Everytime I tried using the IndexBuffer, my GPU crashed on me.

    After trying to simplify the code for a day or two, I started browsing the internet and found out that the indexbuffer can only be filled with SHORT, not INT.

    changed the code to:

    _ib = new IndexBuffer(typeof(short), _indices.Length, _device, Usage.WriteOnly, Pool.Managed);

    and:

    _indices[index++] = (short)(x + (z * _numberOfVerticesX));

    and:

    _indices = new short[numIndices];

    // Array for the indexes of the vertices

           private short[] _indices;

    All in the terrain class. Working fine now. Guess I will move on to the next part now.. hope there will be more chapters some time soon.

  • L4ngfordL4ngford

    Hi, nice tip TankCreator Smiley

    Once again, great tutorial. I've created a Light class, so I  can quickly create many different lights, varying in types, easier, and stored them all in a list, with names, so I can toggle them at will Big Smile

    I am having a problem with the terrain, however: The heightmap loads, but either the light doesn't shine down on the terrain ( and I've tried fixing that Tongue Out ), or the lights arent' getting enabled, which is what I'm thinking, or there's something I'm doing wrong with the material on the terrain itself.

    Anyone else having this problem? or know how to fix objects rendering all black?

    I noticed in your example here that when the lights are off, the car is black, and with lights on, it's textured.

    I'm just trying to see my terrain textured, I can see it as a point list. Also, while viewing it as a trianglestrip, in wireframe, or point mode, The graphics aren't clearing from the screen before the next frame renders. I'm not sure why this happens either :/

    Anyway, I'll continue to fiddle, I know this tutorial is old now but you've still got my compliments Smiley

  • iceteaicetea

    I had also a problem with the lights. I have done it like the source code in the tutorial but it doesn't shine on the obstacles and tanks. I also created a point light but it doesn't shine too.

  • kaplankaplan

    best tutorial about "direct3d and c#" i have read until now. i have experience as c# web developer but still some issues at fully understanding the code but so far i have learnt many great things. thanks a lot...

  • Clint RutkasClint I'm a "developer"

    This is Part 4 of an introductory series on game programming using the Microsoft .NET Framework and managed

  • Clint RutkasClint I'm a "developer"

    This is Part 2 of an introductory series on game programming using the Microsoft .NET Framework and managed

  • AustinAustin

    Is there any reason the following should not work? I want to note that I am not trying to build or anything else, I simply just want to be able to create a DirectX font.

    Imports Microsoft.DirectX

    Imports Microsoft.DirectX.Direct3d

    Public Class Form1

       Private font as Microsoft.DirectX.Direct3D.Font

    End Class

    I keep getting an error message stating, 'Type "Microsoft.DirectX.Direct3D.Font" is not defined.' I cannot figure out for the life of me why this is not working.  Thanks in advance.

  • ge-forcege-force

    I'm having a problem, because whenever I try to display a mesh in this code it gives me a Direct3DXExeption.

  • ge-forcege-force

    Thanks for the tip! I was doing a little custom coding for explosions and shooting... That was really getting me!

  • Clint RutkasClint I'm a "developer"

    @ge-force are you using custom code or the sample code provided in the sample.  Also managed direct-x is no longer supported.  I'd heavily suggest using XNA instead.

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.