作者:clayman
三维化三角形
再来看看我们的程序,看起来并不是那么“三维”。而且我们所做的都能用GDI+轻易完成。So,我们应该怎样在3维空间里绘图,并且给人留下深刻的印象呢?实际上,简单的修改就能达到这样的效果。
如果你还记得,先前在我们创建第一个三角形的时候,我们使用了一个叫做“经过变换的”(transformed)坐标系统。这种坐标是显示器的屏幕区所使用的坐标,也是最容易定义的。如果我们使用未变换过的坐标系统会怎样呢?实际上,未变换过的坐标系统被广泛的用于现代游戏场景。
与屏幕坐标(screem space)相比我们定义这些坐标时,还应在世界坐标(world space)里定义每一个顶点。你可以把世界坐标设想为一个无限大的三维笛卡儿坐标。你可以把你的对象放到这个“世界”的任意位置。现在来修改我们的程序,绘制一个未经过世界坐标变换的三角形。
首先使用未变换顶点格式类型中的一种来改变三角形的数据。在这里我们只关心顶点的位置,以及颜色,因此使用CustomVertex.PositionColored。
CustomVertex.positionColored[] verts = new CustomVertex. positionColored[3];
Verts[0].SetPosition(new Vector3(0.0f,1.0f,1.0f));
Verts[0].Color = System.Drawing.Color.Aqua.ToArgb();
Verts[1]`````````
Verts[2]`````````
(参见DirectX sdk Tutorial 3: Using Matrices)
同样改变VertexFormat属性:
device.VertexFormat = CustomVertex.PositionColored.Format;
好了,现在运行程序:什么也没有发生,仅获得一个填充过的窗口。在讨论为什么之前,先来看看我们都作了些什么。就像你看到的,我们选择了PositonColored结构来保存数据。这个结构用世界坐标保存了顶点的位置,也保存了它的颜色。因为为顶点是没有变换过的,所以我们使用Vector3类来代替Vector4类,没有变换过的顶点是没有rhw值的。Vector3结构的成员直接映射为世界坐标系里x,y,z的值。同时,我们需要确定DirectX3D知道所做的改变,所以我们通过更新VertexFormat属性来让固定功能管道使用新的未变换但填充过颜色的顶点。
So,为什么程序运行时没有正确的显示呢?问题在于,我们只是在世界坐标里绘图,但并没有给DirectX3D任何关于如何来显示它们的信息。我们需要为场景添加一个摄像机来确定如何观看我们的顶点。在经过变换的坐标系统里不需要摄像机的原因是:DirectX3D已经知道在屏幕的哪个位置来显示顶点。
在device上通过两个不同的变换来控制摄像机。每一种变换都被定义为一个4×4的矩阵传递给DirectX3D。(???each transform is defined as a 4*4 matrix that you can pass in to DirectX3D)
映射变换定义了场景被怎样投影到显示器。最简单的产生投影矩阵的方法就是使用Matrix类的PerspectiveFovLH方法。它将会使用左手坐标系创建一个正对场景的透视投影变换。(关于左右手坐标系的详细内容请参见sdk,或你的高等数学、高等物理教材^_^)DirectX3D通常使用左手坐标系。
以下是投影函数的签名:
public static Matrix PerspectiveFovLH( float fieldOfViewY,float aspectRatio,float znearPlane,float zfarPlane);
投影变换描绘了场景的视见体(注:即可见部分)。视见体是由可视角度和前裁剪面(Near Plane)与后裁剪面(Far Plane)定义的一个平截头体(注:比如四棱锥横截面与底面之间的部分,上帝保佑,你还记得高中几何),在这个平截头体之内的即是可见部分。函数头里的nearPlane和farPlane两个参数,描绘了锥体的边界:farPlane就是锥体的底面,而nearPlane则是横截面。fieldOfView参数描绘了锥体的角度。aspectRatio类似于电视的高宽比,比如,宽银幕电视的高宽比是1.85。你可以用可视区域的宽度来比上高度得出这个值。DirectX3D只绘制在这个平截头体中的物体。
既然我没从来没有进行过投影变换,也就根本不存在一个视见体,应此DirectX3D什么也没有绘制。但是,就算我们进行了投影变换,我们还没有进行包含了摄像机信息的view transform。可以用一下函数完成这个任务:
public static Matrix LookAtLH(Matrix pOut, Vector3 cameraPosition, Vector3 cameraTarget, Vector3 cameraUpVector);
仅仅通过各变量的名字你就可以知道如何使用这个函数。其中三个是用来描述摄像机的属性:它的位置、它观察点的位置以及一个被参考为“up”的方向。有了投影变换和view transform的帮助,DirectX3D已经有足够的信息来绘制三角了。添加代码:(参见DirectX sdk Tutorial 3: Using Matrices中的SetupMatrices()函数)
再运行一次试试,哦,我们已经有一个三角了,不过它完全是黑色的!问题在哪呢?在没有经过变换的环境里,DirectX3D默认使用灯光来计算场景中几何体每一个象素的颜色,我们没有定义灯光,也没有额外的光照在三角上,So,它完全是黑色的。既然我们已经为每一个点定义过了颜色,所以现在,可以安全并且简单的把场景里的灯关了。加上如下带码:
dev.RenderState.Lighting = false;
再试一次,终于,我们回到了未变换坐标前的样子。做了这么多改变到底有什么好处呢?最大的好处就是和在屏幕上直接绘制相比我们获得了一个三维空间里的三角形——迈向伟大三维作品的第一步!^_^
既然有了三维空间里的三角,我们怎样做才能让他看起来确实是一个三维空间里的三角呢?最简单的事就是让它旋转起来。如何来做呢?很简单,我们只需要更改世界坐标就可以了。
Device的世界坐标变换会把每一个用局部坐标定义的顶点位置转换为用世界坐标定义的顶点位置。(the world transform on the device is used to transform the objects being drawn from model space ,whice is where each vertex is defined with respect to the model, to world space,where each vertex is actually placed in the world.)Matrix对象的很多方法能完成这种变换:
device.Transform.World = Matrix.RotationZ( (float)Math.PI/6.0f );
它告诉DirectX3D除非指定一个新的世界坐标变换,否则在这段代码之后所有绘制的对象都将进行这种变换。以上的世界坐标变换是根据所给的弧度旋转x轴。注意这里的参数必须是弧度而不是角度。有规率的改变参数的值就能让三角形平滑的转动起来了(以下代码略,参考sdk中的示例)。
我们旋转的三角并不能给人留下深刻的印象。我们来试试让他变得特别一点,并且同时旋转多个轴。很幸运,恰好有这样一个方法,好了,更新代码:
device.Transform.World = Matrix.RotationAxis( new Vector3(angle/((float)Math.PI*2.0f), angle/((float)Math.PI*4.0f), angle/((float)Math.PI*6.0f)), angle/((float)Math.PI);
这里使用了RotationAxis函数,通过这个函数,我们先定义了旋转轴,并在每一维上用一个简单的式子不停改变轴的位置,然后再传入三角形围绕着轴旋转的角度,就像我们先前做的一样。
再次运行程序,哦,我们确实得到了一个围绕着旋转轴转动的三角形,但似乎三角形会有规律的消失一阵,然后再显示出来。好了,还记得我们先前提到的背面剔除(back face culling)吗?这就是背面剔除在起作用的最好例子。当DirectX3D渲染物体的时候,如果它发现某一个面没有对着摄相机,就不会绘制它,这就叫做背面剔除。那么程序在运行时,又是怎样知道某一个特定的几何面是否对着摄像机呢?快速的看看DirectX3D中的裁剪选项或许能给你一点提示。三种可用的剔除选项分别是:none,clockwise(顺时针)以及counterclockwise(逆时针)。在clockwise以及counterclockwise的情况下,当简单几何体的顶点排列顺序与剔除模式相反时,它就不会被绘制。
看看我们的三角形,它的顶点是按逆时针顺序来排列的(注:有关顶点的排列顺序,可参考sdk文档Face and Vertex Normal Vectors)。DirectX3D默认的剔除模式就是逆时针模式。
你可以简单在顶点集合中把第一个和第三个元素交换一下,看看会有什么不同。
现在我们知道背面剔除是怎样工作的,很显然,我们简单的程序并不需要剔除功能。有一个简单的render state来控制剔除模式,添加如下代码:
Device.RenderStates.CullMode = Cull.None;
再一次,erying works as expected,试试拖放窗口的大小会怎样??
添上关于投影变换的图片一张
摄像机位于o点,视见体为ABCD--A'B'C'D',ABCD为后裁剪面,A'B'DC'D'前裁剪面