65.9K
CodeProject 正在变化。 阅读更多。
Home

在托管 DirectX 9 和 C# 中进行 3D 地形可视化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (67投票s)

2005 年 9 月 18 日

6分钟阅读

viewsIcon

367380

downloadIcon

4727

在这个项目中,我将演示如何用相对较少的代码编写一个简单的 3D 渲染应用程序。

Point, Wireframe and Solid (Textured) Mode

引言

GIS(地理信息系统)是一个使用地图来表示数据的计算机支持系统。它帮助人们访问、显示和分析具有地理内容和意义的数据。对于不熟悉 GIS 的人来说,它曾经是一个由 Intergraph、Bentley、MapInfo、Autodesk 和 ESRI 等传统 GIS 和 CAD 公司主导的小众 IT 市场。如今,微软、谷歌和甲骨文等全球 IT 巨头正通过 Virtual Earth、Google Earth 和 Oracle Spatial 等产品争夺市场份额。NASA 最近也发布了一款名为 World Wind 的免费开源 GIS 查看器应用程序。

在本文中,我将演示如何使用 C# 和 Managed DirectX 9.0c 从零开始构建一个独立的 3D 地形可视化工具。该应用程序将允许用户使用箭头键旋转视角,并将渲染模式更改为点(P)、线框(W)和实体(S)。

背景

我最近为当地一个市议会完成了一个 GIS 系统的实施。在该项目中,我开发了一个概念验证应用程序,以证明使用现有的高程点和航空摄影纹理进行 3D 可视化的技术可行性。本文旨在与所有对 GIS 和 .NET 感兴趣的开发者分享我的知识和经验。

要求

在开始之前,我想说明一下这个项目的软件要求:

  • Visual Studio .NET IDE(我使用的是 2005 beta 2)
  • Managed DirectX 9.0c SDK(我使用的是 2005 年 8 月的更新)
  • .NET 框架(我使用的是 v2.0,但 v1.1 也可以)

3D 渲染概念

首先,我需要解释一下我代码背后的通用 3D 编程概念。不幸的是,关于这个主题的书籍有很多,我无法对代码中的每一行都进行全面解释,但我会尝试介绍 3D 可视化背后最重要的思想。

为了生成任何 3D 地形模型,您需要一些基于网格的数据,其中包含每个网格点的 X、Y 和 Z 值。一个非常重要的考虑因素是 Z 值的存储方式,因为 DirectX 使用左手坐标系,而 OpenGL 使用右手坐标系(要了解不同的坐标系,请在互联网上搜索)。我选择的网格大小是 79x88,这仅仅是因为我的源数据是这样存储的,但您可以将其更改为任意网格大小。同样,我的数据使用 20 米的分辨率,这意味着两个相邻点之间的实际距离是 20 米。

Point Mode

一旦读入所有点,您就需要生成一个“网格 (mesh)”。网格是由您在上一步中加载的点构成的三角形数组。3D 中的所有渲染都基于三角形和三角形数组。

Wireframe Mode

即使是当今市场上最强大的显卡,其渲染能力(以每秒可绘制的三角形数量来衡量)也是有限的。因此,显卡需要执行的工作越少,应用程序运行得就越快。这就是优化算法发挥作用的地方,例如 ROAM 或 PLOD(后者内置于 DirectX 9 中)。这些以及其他类似的算法旨在降低距离视点最远处的细节层次和三角形数量。换个角度看,可以说我们在最不重要的地方降低细节层次,而在最重要的地方保留尽可能高的细节层次。我们在这里不会使用这些算法,但您应该了解它们是什么以及它们的用途。

最后,纹理用于提供更逼真的场景外观。纹理使用自己的坐标系,左上角代表 (0,0),右下角代表 (1,1)。此范围(0,0 - 1,1)内的任何纹理点都称为“纹素 (texel)”。

Textured Mode

留给读者的一个练习是,可以进一步增强功能,包括天空盒(SkyBox)、光照(Lighting)、着色(Shading),甚至带有碰撞检测的物理引擎等。Managed DirectX 还支持 DirectPlay 和 DirectSound,提供了高级的网络和声音 API。发挥你的想象力,天空才是极限!

使用代码

我使用 Visual Studio 2005 IDE 构建了一个示例 WinForm 应用程序。您需要在电脑上安装 Managed DirectX 9.0c SDK,项目才能正确编译和运行(我使用的是 2005 年 8 月的更新)。但是,如果您希望将代码分发给未安装 Managed DirectX SDK 的用户,您可以使用小得多的 DirectX 9.0c 可再发行组件包。

好了,让我们深入代码。

首先,我们将导入必要的库:

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;

然后,我们将声明网格的宽度和高度、屏幕和键盘设备、VertexBufferIndexBufferTexture、顶点和三角形的 struct 结构,以及一些在整个项目中使用的其他变量。

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;

现在我们准备初始化我们的 Direct3D 设备对象:

// 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);

如您所见,我们已经连接了 OnDeviceReset 事件,该事件在用户每次调整应用程序窗口大小时触发。我们的点存储在 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);

然后,我们需要实例化一个 IndexBuffer,我们的三角形网格就是由它构建的。IndexBuffer 存储了指向顶点数据的有序列表:

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);

另外,请注意附加源代码中的 InitialiseIndices()LoadHeightData() 函数,我们在这些函数中加载并“三角化”我们的数据。接下来,我们初始化键盘设备:

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

然后,我们定位我们的摄像机:

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;
}

快完成了,只剩下几步了。现在我们将重写 OnPaint 事件,并提供我们自己的事件处理代码:

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();
}

最后,我们需要编写我们的主程序,然后就大功告成了:

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);
  }
}

现在编译并运行。您应该会得到如顶部图片所示的结果。使用键盘上的箭头键来操作应用程序,并按 P、W 和 S 键在不同的渲染模式之间切换:点、线框和实体。很酷,是吧?

注意事项

您很快就会体会到的一件事是,3D 编程是多么耗时。即使是您希望实现的最小细节或效果,也可能需要花费数天痛苦的努力;然而,一旦克服了障碍,整个体验是非常有益的。我给每个人的建议是,首先利用互联网搜索您试图解决的问题。很有可能,已经有人做过您正在尝试做的事情,而且更好的是,问题可能已经被记录并解决了。如果幸运的话,您可能会找到现成的分步教程或代码片段,向您展示如何解决问题。

参考文献

  • 对于那些手头宽裕的人来说,有一本关于在 C# 中进行 Managed DirectX 编程的优秀书籍:
    • 《Managed DirectX 9 - 图形与游戏编程》,作者 Tom Miller。
  • 此外,作为一个起点,这里有一个使用 C# 的优秀 DirectX 9 教程。
  • 另一个提供大量 DX9 和 C# 实践示例的好网站是这里

历史

  • 2005年9月19日:首次发布。
© . All rights reserved.