托管 Direct3D 9 入门 - 创建 3D 时钟 (更新于 2004/6/29)






3.27/5 (12投票s)
2004年6月26日
6分钟阅读

112432

3909
本文将向您展示如何创建一个简单的 Direct3D 应用程序。您将使用简单的网格和纹理创建一个三维时钟。
引言
本项目使用 C# 编写,并使用了托管 Direct X 9。您需要安装 SDK 才能使用它。代码是用 Visual Studio 7.1 创建的。
这个项目是一个时钟。时钟的每个数字都由页面表示。每页顶部是数字的上半部分,底部是数字的下半部分。两页并排放置,一页显示数字的上半部分,另一页显示数字的下半部分。将它们组合在一起就是一个完整的数字。当这样显示时,您可以翻动页面,从一个数字切换到另一个数字。以图片中的最后一个数字为例,这很难用文字解释。
我的代码有相当好的文档注释,所以您可能只需要查看代码就能了解它的实现方式。在这里,我将讨论一些更重要的部分。
为了做任何事情,我们需要获取我们将要绘制到的设备。我们还需要设置该设备。以下代码完成了这些操作:
private static Device device = null;
// PresentParameters are used for the display device
PresentParameters presentParams = new PresentParameters();
// this is a windowed application
presentParams.Windowed = true;
// set the swap effect
presentParams.SwapEffect = SwapEffect.Discard;
// tell the device to use depth information
// and format to use, this will insure that things that
// should be infront will be
presentParams.AutoDepthStencilFormat = DepthFormat.D16;
presentParams.EnableAutoDepthStencil = true;
// create our display device
// we are using the default display device, and doing
// the vertex processing with the cpu, this will insure
// compatability with older cards
device = new Device(0, DeviceType.Hardware, this,
CreateFlags.SoftwareVertexProcessing, presentParams);
// we subscribe to the device reset event. A reset occurs
// when something occurs with our window
//such as resizing.
// We need to capture this event because during a reset
// we lose any information in video memory.
//Therefore we need to setup some stuff again.
device.DeviceReset += new EventHandler(this.OnDeviceReset);
// Set up the device now
this.OnDeviceReset(device, null);
我们需要响应 DeviceReset
事件,以便我们的应用程序能够应对视频内存丢失的情况。在设备重置事件中,我们设置了我们要绘制的场景。这包括我们可以看到的场景范围(也称为视锥体)。这是通过设置投影矩阵来实现的。通过设置投影矩阵,我们定义了近平面和远平面,以及相机视角。这个角度决定了近平面和远平面的可见范围。我们还设置了相机。这是通过设置视图矩阵来实现的。我们需要提供相机位置、相机目标点以及相机“向上”方向的信息。
// Set up the projection matrix, this defines the volume
// that is visible on the screen, and its perspective.
device.Transform.Projection =
Matrix.PerspectiveFovLH((float)Math.PI/4,
this.Width/this.Height,1.0f,100.0f);
// Set up the view matrix. This is our camera.
// The camera will be located along the z axis,
// looking at the origin, which is where we will display the clock.
device.Transform.View = Matrix.LookAtLH(new Vector3(0,0,10f),
new Vector3(),new Vector3(0,1,0));
我在 DeviceReset
中选择做的其他事情是设置光照。我需要使用光照,因为我将使用一个不包含颜色信息的网格。最简单的光照类型是环境光。环境光会均匀地影响场景中的所有对象。我们将环境光设置为白色,因此对象会显示其真实的颜色。
// turn on lighting
device.RenderState.Lighting = true;
// place a ambient white light in our scene. An ambient light
// touches all surfaces that we draw, and
// affects them all equally. We set it to white so that the surfaces
// will look the color they actually are.
device.RenderState.Ambient = Color.White;
现在我们的设备已经设置好了,我们可以在 OnPaint
函数中绘制任何我们想要的东西。我将首先讨论绘制点,然后讨论绘制数字。点是使用网格绘制的。我们使用 Direct3D 的内置网格类型之一。创建网格时,我们需要告诉它将使用哪个设备以及我们想要的尺寸和密度信息。默认情况下,网格不包含颜色信息。要为网格添加颜色信息,我们使用材质。我们创建一个在环境光照射下看起来像黑色的材质。
// create the mesh for our dot
dotMesh = Mesh.Sphere(device, .2f, 18, 18);
// because the default sphere mesh contains
// no color information for it's vertices,
// we will use ambient lighting to provide this color.
// the dot should reflect ambient light as if it is a black surface
// the color the dot will actually appear is based
// on its color and the color of the light
dotMaterial = new Material();
dotMaterial.Ambient = Color.Black;
现在我们有了网格,我们可以轻松地绘制它。我们首先需要告诉设备使用哪个材质和哪个纹理。我们的网格没有纹理,所以我们将其设置为 null
。
// set the material and texture. These are properties
// of the device. All things drawn
// will use this material and texture.
// set the material to the dot material
device.Material = dotMaterial;
// set the texture to null
device.SetTexture(0,null);
我们对网格如何绘制的最终控制是通过世界变换来完成的。默认情况下,我们的网格位于原点。我们将使用平移(移动)将网格放置在我们想要放置点的位置。
// translate (move along the x,y,z axis) all things draw
device.Transform.World = Matrix.Translation(2.5f,.6f,0);
现在,我们绘制网格。每个网格由子集组成,这些子集是网格的小片段。绘制网格时,您需要绘制其每个子集。但是,由于我们的网格非常简单,它只有一个子集。要实际绘制我们的网格,我们就绘制这个子集。请注意,我们不需要指定绘制到哪个设备,因为我们已经告诉了网格它应该用于哪个设备。
// draw one dot
dotMesh.DrawSubset(0);
现在,我们将讨论绘制数字。在我的代码中,每个数字都是一个类,通过调用其 Draw
函数来绘制。我将详细介绍类如何绘制自身。我们数字的每一页都有正面和背面。例如,其中一页的正面是数字 0 的上半部分,背面是数字 1 的上半部分倒置。这样,当 0 的上半部分向下翻转时,1 的下半部分就会在正确的位置可见。每一页都有 2 个相关的纹理。纹理基本上是一个位图。纹理包含我们刚才谈到的数字半部分的位图。以下代码从嵌入式资源加载所有纹理:
texTop = new Texture[10];
texTop[0] = new Texture(device, new Bitmap(this.GetType(),
"0top.bmp"),Usage.Dynamic, Pool.Default);
texTop[1] = new Texture(device, new Bitmap(this.GetType(),
"1top.bmp"),Usage.Dynamic, Pool.Default);
texTop[2] = new Texture(device, new Bitmap(this.GetType(),
"2top.bmp"),Usage.Dynamic, Pool.Default);
texTop[3] = new Texture(device, new Bitmap(this.GetType(),
"3top.bmp"),Usage.Dynamic, Pool.Default);
texTop[4] = new Texture(device, new Bitmap(this.GetType(),
"4top.bmp"),Usage.Dynamic, Pool.Default);
texTop[5] = new Texture(device, new Bitmap(this.GetType(),
"5top.bmp"),Usage.Dynamic, Pool.Default);
texTop[6] = new Texture(device, new Bitmap(this.GetType(),
"6top.bmp"),Usage.Dynamic, Pool.Default);
texTop[7] = new Texture(device, new Bitmap(this.GetType(),
"7top.bmp"),Usage.Dynamic, Pool.Default);
texTop[8] = new Texture(device, new Bitmap(this.GetType(),
"8top.bmp"),Usage.Dynamic, Pool.Default);
texTop[9] = new Texture(device, new Bitmap(this.GetType(),
"9top.bmp"),Usage.Dynamic, Pool.Default);
texBottom = new Texture[10];
texBottom[0] = new Texture(device, new Bitmap(this.GetType(),
"0bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[1] = new Texture(device, new Bitmap(this.GetType(),
"1bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[2] = new Texture(device, new Bitmap(this.GetType(),
"2bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[3] = new Texture(device, new Bitmap(this.GetType(),
"3bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[4] = new Texture(device, new Bitmap(this.GetType(),
"4bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[5] = new Texture(device, new Bitmap(this.GetType(),
"5bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[6] = new Texture(device, new Bitmap(this.GetType(),
"6bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[7] = new Texture(device, new Bitmap(this.GetType(),
"7bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[8] = new Texture(device, new Bitmap(this.GetType(),
"8bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[9] = new Texture(device, new Bitmap(this.GetType(),
"9bottom.bmp"),Usage.Dynamic, Pool.Default);
要实际绘制我们的数字,我们需要绘制纹理将被绘制到的图元(在我们的情况下是三角形)。绘制之所以使用三角形,是因为没有正方形。我们通过将 2 个三角形组合在一起形成一个正方形。因此,我们的每个纹理都将通过指定 6 个点或顶点来绘制。然后,这些尺寸点将被连接起来形成 2 个三角形。一个重要的细节是,我们将以逆时针顺序列出顶点。DirectX 根据顶点的列出顺序来确定我们是否面向纹理。如果不是,除非我们明确告知,否则它不会绘制它。因此,我们的页面实际上由 12 个顶点组成,6 个用于正面,6 个用于背面。我们使用以下代码定义我们的顶点:
// the coordinates in the front face of the page,
// this is the top half of a number
verts[0] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f,
1.0f,0.0f,1.0f,0.0f);
verts[1] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f,
0.0f,0.0f,1.0f,1.0f);
verts[2] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f,
1.0f,0.0f,0.0f,0.0f);
verts[3] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f,
0.0f,0.0f,1.0f,1.0f);
verts[4] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f,
0.0f,0.0f,0.0f,1.0f);
verts[5] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f,
1.0f,0.0f,0.0f,0.0f);
// the coordinate in the back face of the page,
// this is the bottom half of a number
verts[6] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f,
1.0f,0.0f,1.0f,1.0f);
verts[7] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f,
1.0f,0.0f,0.0f,1.0f);
verts[8] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f,
0.0f,0.0f,0.0f,0.0f);
verts[9] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f,
0.0f,0.0f,0.0f,0.0f);
verts[10] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f,
0.0f,0.0f,1.0f,0.0f);
verts[11] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f,
1.0f,0.0f,1.0f,1.0f);
请注意,每个顶点由 5 个数字定义。顶点的 x、y 和 z 坐标,以及应与顶点对齐的纹理的 x 和 y 坐标。(注意:这些坐标被归一化到 0-1 的范围内。)在绘制三角形时,纹理将以一种方式放置在上面,使得纹理坐标与我们分配给顶点的坐标相匹配。
我们将顶点放置在我们使用以下代码创建的顶点缓冲区中。在绘制顶点时,我们将把这个缓冲区提供给设备。
// vertex buffer storing coordinates used when drawing digit
private static VertexBuffer vb = null;
// create vertex buffer
vb = new VertexBuffer(typeof(CustomVertex.PositionTextured),
12,device,Usage.Dynamic | Usage.WriteOnly,
CustomVertex.PositionTextured.Format, Pool.Default);
我们使用以下代码行将顶点放置到缓冲区中:
// take the date from verts and put in our vertex buffer
buffer.SetData(verts,0,LockFlags.None);
在绘制页面之前,我们需要做的最后一件事是设置材质。同样,我们没有为顶点提供任何颜色信息,因此我们需要这样做:
private static Material material;
material = new Material();
material.Ambient = Color.White;
现在,我们绘制页面。首先,我们告诉设备使用什么材质和纹理。我们还告诉它我们的顶点是什么格式,以及在哪里可以找到我们的顶点。为了使我们的页面出现在我们想要的位置和旋转角度,我们更改世界变换。
// set up the material, so light will get reflected
device.Material = material;
// tell the device what format the vertices we will be drawing are in
device.VertexFormat = CustomVertex.PositionTextured.Format;
// tell the device which buffer to get the vertices from
device.SetStreamSource(0,vb,0);
// set texture for front of page and draw it
device.SetTexture(0,top);
// set world transform to draw page at right location and angle
device.Transform.World = Matrix.RotationX(angle) * Matrix.Translation(x,y,z);
我们使用 DrawPrimitives
来实际绘制我们的页面(记住它们实际上是三角形)。我们需要指定图元类型、缓冲区中的起始索引以及要绘制的图元数量。
device.DrawPrimitives(PrimitiveType.TriangleList,0,2);
要绘制背面,我们只需更改纹理并绘制定义背面的三角形。
device.SetTexture(0,bottom);
device.DrawPrimitives(PrimitiveType.TriangleList,6,2);
我们对构成数字的每个页面重复这些过程。
希望您能理解并遵循我的解释。如有任何疑问,请随时提问。
尽情享用!!