Beginning Game Development: Part VI - Lights, Materials and Terrain
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 Rendermethod. 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 theDirect3D 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 theRenderFonts 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 theRenderState 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:
- 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.
- 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.
- 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.
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.
- Ambient: Ambient light is the general background light and is the same everywhere in the scene.
- 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.
- 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.
- 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 theConfigureDevice 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). TheDeviceCaps.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 theGameEngine 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:
- Load the height information from the height map into an array.
- Store the vertices for the regular grid mesh in a vertex buffer.
- Store the indexes for the regular grid mesh in an index buffer.
- Compute the normals for each triangle.
- 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.vcoordinates 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 VertexBufferand 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 correspondingDrawIndexedPrimitives 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 theGameEngine 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 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 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.
0 comments:
Post a Comment