Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

3D Terrain Visualisation in Managed DirectX 9 and C#

0.00/5 (No votes)
18 Sep 2005 1  
In this project I demonstrate how to write a simple 3D rendering application in a relatively small amount of code.

Point, Wireframe and Solid (Textured) Mode

Introduction

GIS (Geographical Information System) is a computer support system that represents data using maps. It helps people access, display and analyse data that has geographic content and meaning. For those not familiar with GIS, it used to be a niche IT market dominated by the traditional GIS and CAD companies such as Intergraph, Bentley, MapInfo, Autodesk and ESRI. Nowadays global IT giants such as Microsoft, Google and Oracle are competing for their share of the pie through products such as Virtual Earth, Google Earth and Oracle Spatial. NASA has also recently released a free, open source GIS viewer application called World Wind.

In this article, I will demonstrate how to build a standalone 3D terrain visualisation tool from scratch using C# and Managed DirectX 9.0c. The application will allow the user to rotate the point of view using the arrow keys and to change the rendering mode to (P) Point, (W) Wire frame and (S) Solid.

Background

I recently completed a GIS system implementation for a local City Council. During that project I developed a proof of concept application to demonstrate the technical feasibility of 3D visualisation using the available spot heights and aerial photography textures. The aim of this article is to share my knowledge and experience with all developers interested in GIS and .NET.

Requirements

Before we start, I would like to specify the software requirements for this project:

  • Visual Studio .NET IDE (I used 2005 beta 2)
  • Managed DirectX 9.0c SDK (I used August 2005 update)
  • .NET framework (I used v2.0 but v1.1 will work as well)

3D rendering concepts

First of all I will need to explain the general 3D programming concepts behind my code. Unfortunately, entire books are written on this topic and I won't be able to give a full explanation for every single line in my code, but instead will attempt to present the most important ideas behind 3D visualisation.

In order to generate any 3D terrain model, you will need some grid based data with X, Y and Z values for each grid point. A very important consideration is how the Z value is stored, as DirectX uses left-handed coordinate system while OpenGL uses right-handed coordinate system (to learn about different coordinate systems, please search the Internet). I have chosen a grid size of 79x88 simply because that is how my source data is stored, but you can change this to any arbitrary grid size. Likewise, my data uses 20m resolution which means that the real distance between two adjacent points is 20 meters.

Point Mode

Once you read in all the points you will need to generate a "mesh". The mesh is an array of triangles constructed from the points you loaded in the previous step. All rendering in 3D is based on triangles and arrays of triangles.

Wireframe Mode

Even the meanest video cards on today's market have limited rendering capability in terms of how many triangles can be drawn per second. Therefore, the less work your video card needs to perform the faster your application runs. This is where optimisation algorithms come into play, such as ROAM or PLOD (the latter is built-in DirectX 9). These and other similar algorithms are aimed at reducing the level of detail and number of triangles located furthest from the view point. The other way to look at this is to say that we are reducing the level of detail where it matters least, while we are preserving the highest possible level of detail where it matters most. We won't be using these algorithms here, however you should be aware of what they are and what they are used for.

Finally, textures are used to provide a more realistic look of the scene. Textures use their own coordinate system, with top left representing 0,0 and bottom right representing 1,1. Any texture point within this range (0,0 - 1,1) is referred to as a "texel".

Textured Mode

As an exercise left to the reader, further enhancements could include the SkyBox, Lighting, Shading or even Physics engine with collision detection etc. Managed DirectX also includes support for DirectPlay and DirectSound with advanced networking and sound APIs. Using your imagination, the sky is the limit!

Using the code

I have built a sample WinForm application using the Visual Studio 2005 IDE. You will need to have Managed DirectX 9.0c SDK installed on your PC for the project to compile and run correctly (I used the August 2005 update). However you can use a much smaller DirectX 9.0c redistributable if you wish to distribute your code to users who don't have Managed DirectX SDK installed on their PC.

OK, let's dive into the code.

First of all, we will import the necessary libraries:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.IO;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using Microsoft.DirectX.DirectInput;

Then, we will declare our grid width and height, screen and keyboard devices, VertexBuffer and IndexBuffer, Texture, Vertex and Triangle structs and a few other variables used throughout the project:

private int GRID_WIDTH = 79;     // grid width

private int GRID_HEIGHT = 88;    // grid height

private Microsoft.DirectX.Direct3D.Device device = null;  // device object

private Microsoft.DirectX.DirectInput.Device keyb = null; // keyboard

private float angleZ = 0f;       // POV Z

private float angleX = 0f;       // POV X

private float[,] heightData;     // array storing our height data

private int[] indices;           // indices array

private IndexBuffer ib = null; 
private VertexBuffer vb = null;
private Texture tex = null;
//Points (Vertices)

public struct dVertex
{
  public float x;
  public float y;
  public float z;
}
//Created Triangles, vv# are the vertex pointers

public struct dTriangle
{
  public long vv0;
  public long vv1;
  public long vv2;
}
private System.ComponentModel.Container components = null;

Now we're ready to initialise our Direct3D device object:

// define parameters for our Device object

PresentParameters presentParams = new PresentParameters();
presentParams.Windowed = true;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.EnableAutoDepthStencil = true;
presentParams.AutoDepthStencilFormat = DepthFormat.D16;
// declare the Device object

device = new Microsoft.DirectX.Direct3D.Device(0, 
             Microsoft.DirectX.Direct3D.DeviceType.Hardware, this, 
             CreateFlags.SoftwareVertexProcessing, presentParams);
device.RenderState.FillMode = FillMode.Solid;
device.RenderState.CullMode = Cull.None;
// Hook the device reset event

device.DeviceReset += new EventHandler(this.OnDeviceReset);
this.OnDeviceReset(device, null);
this.Resize += new EventHandler(this.OnResize);

As you can see we have wired up the OnDeviceReset event which fires up every time the user resizes the application window. Our points are stored in a VertexBuffer:

// create VertexBuffer to store the points

vb = new VertexBuffer(typeof(CustomVertex.PositionTextured), 
         GRID_WIDTH * GRID_HEIGHT, device, Usage.Dynamic | Usage.WriteOnly, 
         CustomVertex.PositionTextured.Format, Pool.Default);
vb.Created += new EventHandler(this.OnVertexBufferCreate);
OnVertexBufferCreate(vb, null);

Then, we need to instantiate an IndexBuffer, from which our triangular mesh is constructed. IndexBuffer stores an ordered list into the Vertex data:

ib = new IndexBuffer(typeof(int), (GRID_WIDTH - 1) * 
     (GRID_HEIGHT - 1) * 6, device, Usage.WriteOnly, Pool.Default);
ib.Created += new EventHandler(this.OnIndexBufferCreate);
OnIndexBufferCreate(ib, null);

Also pay attention to InitialiseIndices() and LoadHeightData() functions in the source code attached, where we load and "triangulate" our data. Next, we initialise the keyboard device:

public void InitialiseKeyboard()
{
  keyb = new Microsoft.DirectX.DirectInput.Device(SystemGuid.Keyboard);
  keyb.SetCooperativeLevel(this, CooperativeLevelFlags.Background |
                           CooperativeLevelFlags.NonExclusive);
  keyb.Acquire();
}

Then, we position our camera:

private void CameraPositioning()
{
  device.Transform.Projection = 
     Matrix.PerspectiveFovLH((float)Math.PI/4,   
     this.Width/this.Height, 1f, 350f);
  device.Transform.View = 
     Matrix.LookAtLH(new Vector3(0, -70, -35), new Vector3(0, -5, 0), 
     new Vector3(0, 1, 0));
  device.RenderState.Lighting = false;
  device.RenderState.CullMode = Cull.None;
}

Almost done, only a few steps left. Now we will override the OnPaint event and provide our own event handling code:

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
  device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.LightBlue , 1.0f, 0);
  // set the camera position

  CameraPositioning();
  // draw the scene     

  device.BeginScene();
  device.SetTexture(0, tex);
  device.VertexFormat = CustomVertex.PositionTextured.Format;
  device.SetStreamSource(0, vb, 0);
  device.Indices = ib;
  device.Transform.World = 
         Matrix.Translation(-GRID_WIDTH/2, -GRID_HEIGHT/2, 0) *   
         Matrix.RotationZ(angleZ)*Matrix.RotationX(angleX);
  device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, GRID_WIDTH * 
                               GRID_HEIGHT, 0, indices.Length/3);
  device.EndScene(); 
  device.Present();
  this.Invalidate();
  ReadKeyboard();
}

Finally, we need to write our main procedure and we're done:

static void Main() 
{
  using (WinForm directx_form = new WinForm())
  {
    directx_form.LoadHeightData();
    directx_form.InitialiseIndices();
    directx_form.InitialiseDevice();
    directx_form.InitialiseKeyboard();
    directx_form.CameraPositioning();
    directx_form.Show();
    Application.Run(directx_form);
  }
}

Now compile and run. You should get the results as shown in the pictures at the top. Use the arrow keys on your keyboard to drive the application, and press P, W and S to switch between different rendering modes: Point, Wire frame and Solid. Cool, huh?

Points to note

One thing you'll quickly learn to appreciate is how time consuming 3D programming can be. Even the smallest detail or effect you wish to implement could take many painful days; however, the entire experience is very rewarding once you overcome the hurdles. My suggestion to everyone is to use the Internet in the first instance and search about the problem you're trying to solve. The chances are, someone has already done what you're trying to do, and better still, the problem may have been documented and solved. If you're lucky, a step-by-step tutorial or code snippets may be readily available showing you how to solve your problem.

References

  • For those people who have some spare cash in their wallets, there is an excellent book available for Managed DirectX programming in C#:
    • Managed DirectX 9 - Graphics and Game Programming, by Tom Miller.
  • Also, as a starting point there is an excellent DirectX 9 tutorial using C# available here.
  • Another great site with lots of practical examples in DX9 and C# is available here.

History

  • 19th September 2005: First release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here