Pac-Man for the Smartphone

This content is no longer current.
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.
The cleanup for this article consists mainly of version upgrades and some minor performance improvements.
presentParams.AutoDepthStencilFormat = DepthFormat.D16;
presentParams.EnableAutoDepthStencil = true;
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.
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:
DirectX breaks light into two groups:
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.)
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.
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.
Now enough with the theory, and on to the code.
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.
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.
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:
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 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.
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.
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.
im mulimedia & Graphic Programmer And
I Want professional sample From Directx By C#
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
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!!!
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.
@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.
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!!!!
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.
Hi, nice tip TankCreator
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
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 ), 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
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.
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...
This is Part 4 of an introductory series on game programming using the Microsoft .NET Framework and managed
This is Part 2 of an introductory series on game programming using the Microsoft .NET Framework and managed
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.
I'm having a problem, because whenever I try to display a mesh in this code it gives me a Direct3DXExeption.
Thanks for the tip! I was doing a little custom coding for explosions and shooting... That was really getting me!
@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.