分类: C/C++
2008-02-20 12:44:00
Device类是DirectX里的所有绘图操作所必须的。可以把这个类假想为真实的图形卡。场景里所有其他图形对象都依赖于device。你的计算机里可以有一个到几个device,在Mnaged DirctX3D里,你可以控制任意多个device。
Device共有三个构造函数,我们现在只讨论其中的一个,但会在后边的内容里讨论其他的。先来看看具有如下函数签名的构造函数:
public Device(int adapter,DeviceType deviceType,Control renderWindow,CreateFlags behaviorFlags, PresentParameters[] presentationParameters);
(构造函数的第二种重载类似于上边这个,但它接受来自非托管(或者非windows form)的窗口句柄作为renderWindow。而只接受一个IntPtr参数的重载是非托管com组建指向Idirect3Ddevice9的接口。当你的代码需要和非托管的程序协作时则应用它)
好了,这些参数是什么意思,以及我们怎样来使用呢?呵呵,参数adapter表示我们将要使用哪个物理图形卡。计算机里的所有图形卡都有一个唯一的适配器标识符(通常是0到你的图形卡数量-1),默认的显卡总是标识为0 的图形卡。
下一个参数,DeviceType,告诉了DirectX3D你要创建哪种类型的device。这里最常用的值是DeviceType.Hardware,表示你将创建一个硬件设备。另一个选项DeviceType.Reference,这种设备允许你使用“参考光栅器”(reference rasterizer),所有的效果由DirectX3D运行时来实现,以很慢、很慢、很慢的速度运行^_^。应该仅在调试或测试你的显卡不支持的特性时使用这个选项。
(注意参考光栅器只包含在DirectX SDK里,so DirectX运行时是不能使用这个特性的。最后一个为DeviceType.Software的值允许使用用户自定义的软件光栅器(custom software rasterizer)在不确定是否有这样一个光栅器存在时,忽略这个选项吧^_^。)
rendrWindow表示把设备绑定到的窗口。因为windows form控件类都包含了一个窗口句柄(windows handle),所以很容易把一个确定的类作为渲染窗口。可以使用form、panel或其他任意的控件作为这个参数的值。但现在,我们只用form。
下一个参数用来描述设备创建之后的行为。大部分CreateFlags枚举的成员都能组合起来使用,使设备具有多种行为。但有一些flag是相互排斥的,稍后讨论它们。我们现在只使用SoftwareVertexProcessing标志。这个标志适合于所有顶点处理都用CPU计算的情况。因此,这自然比所有点都用GPU处理要慢,因为我们不确定你的显卡是否支持所有特性。So,安全第一,假设你的CPU能完成现在的任务。
最后一个参数,它表示你的设备把数据呈现到显示器的方式。Presentation Parameter类的外观都可以由这个类来控制。我们过后再来深入讨论它的构造函数,现在,我们只关心“Windowed”成员和“SwapEffect”成员。
Windowed成员是一个布尔类型的值,决定设备是全屏还是窗口模式。
SwapEffect成员用于控制缓存交换的行为。如果选择了SwapEffect.Flip,运行时会创建额外的后备缓冲(back buffer),并且在显示时拷贝front buffer。SwapEffect.Copy与Flip相似,但要求你把后备缓冲设为1。我们将要选择的SwaoEffect.Discard,如果缓冲没有准备好被显示,则会丢弃缓冲中的内容(which simply discards the contents of the buffer if it isn’t ready to be presented)。
学了这么多,现在来创建一个设备吧。回到代码上来,首先为我们的程序将创建一个device对象:
(代码略,参见DirectX sdk Tutorial 1: Create a Device)
现在让我们来重写Paint()函数:
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
device.Clear(ClearFlags.Target, System.Drawing.Color.Blue,
device.Present();
}
我们使用Clear()方法把窗口填充为实心的颜色。它的第一个参数指定了我们要填充的对象;在例子里,我们填充的即是目标窗口。稍后再来讨论ClearFlags枚举的其它成员。第二个参数是我们所要填充的颜色。其他的两个参数先暂时忽略。在device被填充之后,我们必须更新显示:Present方法会为我们完成这个任务。这个方法也有几个重载的类型;上边使用的方法会显示device的整个区域。同样稍后再讨论。
看的有些枯燥了吗,好吧,现在我们来真正绘制一些图形
三维图形世界里最基本的图形就是三角形。使用足够的三角,我们可以呈现出任何东西,甚是是平滑的曲面。没有什么比画一个简单的三角形更好的了。为了使过程尽可能的简单,我们先避开“world space”以及各种变换(当然,我们马上就会提到他们),使用屏幕坐标来绘制一个简单的三角。再绘制我们迷人的三角前,我们必须做2件事。1,需要一些数据结构来保存三角的信息。2,告诉device来绘制它。
很幸运,DirectX已经有这样一个数据结构来保存三角了。Direct3D名称空间里叫做CustomVertex的类可以用来储存大多数Direct3D中用到的“顶点格式”数据结构(vertex format)。
一个顶点格式结构把数据保存为一种DirectX3D认识并可以使用的格式。我们将讨论很多这种结构,但先看看我们即将用来创建三角的TransformedColored结构。这个结构告诉DirectX3D运行时我们的三角不需要进行坐标变换(比如旋转或移动),因为我们已经指定了使用屏幕坐标系。它也包含了每一个点(顶点)的颜色的信息。回到重写的OnPaint方法添加如下代码:
CustomVertex.TransformedColored[] verts = new CustomVertex.TransformedColored[3];
Verts[0].SetPosition(new Vector4(this.Width/
Verts[0].Color = System.Drawing.Color.Aqua.ToArgb();
Verts[1]`````````
Verts[2]`````````
(参见DirectX sdk Tutorial 2: Rendering Vertices)
数组里的每一个元素表示三角的一个顶点,所以我们创建了3个元素。然后使用新创建的Vector4结构为每一个成员调用SetPositin方法。变换过的顶点坐标包含了在屏幕上x和y的坐标(相对于屏幕的(0,0)点而言),当然也包括z坐标和rhw成员(reciprocal of homogenous w三维齐次坐标)。先忽略后边两个参数。Vector4结构(注:Vector4其实就是(x,y,z,w)经过变换后成为(x/w,y/w,z/w))是保存这种信息最方便的方式。然后我们设置了点的颜色。注意,我们使用了标准颜色的ToArgb方法。DirectX3D假设所接收的颜色为32位int。
既然有了数据就可以告诉DirectX我们需要绘制这个三角形,并且绘制它。在重写的OnPaint里添加如下代码
device.BeginScene();
device.VertexFormat = CustomVertex.TransformedColored.Format;
device. DrawUserPrimitives (PrimitiveType.TriangleList,1,verts); 注意和sdk中的示例有区别
device.EndScene();
好了,这几行代码是什么意思呢?其实很简单。BefinScene方法告诉DirectX3D我们即将绘制一些东西,为绘制做好准备。现在我们已经告诉了DirectX3D要绘制一些东西,接下来就必须告诉它画什么。这就是VertexFormat属性的作用。它决定了DirectX3D运行时使用哪种“固定功能管道”(fixed function pipline)格式。在我们的例子里使用变换过的,着色过的顶点管道。
不用担心你现在不明白确定的功能管道是什么意思,我们会很快来讨论它。
DrawUserPrimitives函数是真正发生绘图的地方。So,他的参数是什么意思呢?第一个参数是我们要绘制的初等几何体的类型。有很多种可用的类型,but now,我们只是画一系列的三角形。所以选择了PrimitiveType.TriangleList类型。第二个参数是我们要绘制的三角形的数量。对于一个三角形的集合来说,这个值应该是你的顶点数量除以3。我们只画一个三角,所以设为1。最后一个参数则是DirectX3D用来绘图的数据。最后一个EndSence方法通知DirectX3D我们不再绘图了。你必须再每次调用BeginSence之后都调用这个方法。
如果现在编译运行程序,你会发现移动或重置窗口大小之后,并不会更新显示。原因是当我们需要重绘整个窗口时,Windows并不会每一次都计算窗口的收缩情况。因此,你只是移除了显示过的数据,但并没有删除已经显示的内容。很幸运,有个简单的方法解决这个问题,我们可以告诉Windows窗口总是需要被整个的重绘。在OnPaint的最后加上一下代码:
this.Invalidate();
呵呵,现在再来试试看,哦,看起来我们破坏了程序!现在只能显示一片空白了,并且我们的三角还在不停的闪烁,尤其是当调整窗口大小时。我们都干了些什么呢?原来“聪明”的Windows总是尝试在Invalidate()方法后来绘制当前的窗口(即空白的这个窗口)。在我们的OnPaint方法之外还存在其他的绘制过程!能容易的通过改变窗口的“style”属性来解决。在构造函数里加上如下代码
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ConstolStyles.Opaque, true);
哦~,好了,终于erying works as expected。我们所做的就是告诉Windows一切绘图过程都在OnPaint里完成。
三维化三角形
再看看我们的程序,它看起来并不那么“三维”。而且我们所做的都能用GDI+轻易完成。So,我们应该怎样在3维空间里绘图,并且给人留下深刻的印象呢?实际上,简单的修改就能达到这样的效果。
如果你还记得,先前在我们创建第一个三角形的时候,我们使用了一个叫做“经过变换的”(transformed)坐标系统。这种坐标是显示器的屏幕区所使用的坐标,也是最容易定义的。如果我们使用未变换过的坐标系统会怎样呢?实际上,未变换过的坐标系统被广泛的用于现代游戏场景。
与屏幕坐标(screem space)相比我们定义这些坐标时,还应在世界坐标(world space)里定义每一个顶点。你可以把世界坐标设想为一个无限大的三维笛卡儿坐标。你可以把你的对象放到这个“世界”的任意位置。现在来修改我们的程序,绘制一个未经过世界坐标变换的三角形。
首先使用未变换顶点格式类型中的一种来改变三角形的数据。在这里我们只关心顶点的位置,以及颜色,因此使用CustomVertex.PositionColored。
CustomVertex.positionColored[] verts = new CustomVertex. positionColored[3];
Verts[0].SetPosition(new Vector3(
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/
它告诉DirectX3D除非指定一个新的世界坐标变换,否则在这段代码之后所有绘制的对象都将进行这种变换。以上的世界坐标变换是根据所给的弧度旋转Z轴。注意这里的参数必须是弧度而不是角度。有规律的改变参数值就能让三角形平滑的转动起来了(以下代码略,参考sdk中的示例)。
我们旋转的三角并不能给人留下深刻的印象。来试试让他变得特别一点,同时旋转多个轴。很幸运,恰好有这样一个方法,好了,更新代码:
device.Transform.World = Matrix.RotationAxis( new Vector3(angle/((float)Math.PI*
这里使用了RotationAxis函数,通过这个函数,我们先定义了旋转轴,并在每一维上用一个简单的式子不停改变轴的位置,然后再传入三角形围绕着轴旋转的角度,就像先前做的一样。
再次运行程序,哦,我们确实得到了一个围绕着一条旋转的轴转动的三角形,但似乎三角形会有规律的消失一阵,然后再显示出来。好了,还记得我们先前提到的背面剔除(back face culling)吗?这就是背面剔除在起作用的最好例子。当DirectX3D渲染物体的时候,如果它发现某一个面没有对着摄相机,就不会绘制它,这就叫做背面剔除。那么程序在运行时,又是怎样知道某一个特定的几何面是否对着摄像机呢?快速看看DirectX3D中的裁剪选项或许能给你一点提示。三种可用的剔除选项分别是:none,clockwise(顺时针)以及counterclockwise(逆时针)。在clockwise以及counterclockwise的情况下,当简单几何体的顶点排列顺序与剔除模式相反时,它就不会被绘制。
device.Lights[0].Type = LightType.Point;
device.Lights[0].Positon = new Vector3();
device.Lights[0].Diffuse = System.Drawing.Color.White;
device.Lights[0].Attenuation =
device.Lights[0].Range =
device.Lights[0].Commit();
device.Lights[0].Enabled = true;
这些代码什么意思呢?首先申明了要创建的灯光类型,我们选择了一个在所有方向上辐射强度都一样的point light,创造了一个灯泡般的世界。当然,也有灯光沿着指定方向传播的direction light。direction light只会产生方向和颜色上的效果,忽略其他的灯光要素(比如光线的削弱(attenuation)和范围(range)),因此它也是计算量最小的灯光。最后一种能用的就是spot light了,类似于剧场里用来照亮舞台上人物的灯光。有许多的要素来描述spot light(位置,方向,角度,等等),所以它是系统里所需计算量最大的灯光。
在对灯光类型简单的讨论之后,我们继续。接下来设置灯光的位置。因为三角形的中心在(0,0,0),所以我们把灯光也放到那个位置。Vector3无参数的构造函数完成了这个任务。把灯光的漫射颜色设置为白色,这样可以正常的照亮表面。接下来设置控制灯光强度在空间改变的削弱属性。范围是灯光能产生效果的最远距离。例子里的范围已经远远超过了我们所需要的。请查阅sdk寻找有关灯光的更多内容。
最后我们把灯光提交给了device,并使它可用。如果你浏览灯光的属性,会注意到一个叫做“Deferred”的布尔值。默认情况下,这个值是false,所以你需要在准备使用灯光之前调用Commit函数。把这个值设为true,可以取消对Commit的调用,但会带来一定的性能损失。在观看灯光的效果前一定要确定它是enable和committed的。
回到程序,你发现即使我们为场景定义了灯光,三角也还是黑色的!打开了灯,却看不到光,Direct3D一定没有照亮我们的三角形,事实上,它确实没有。只有在几何体的每一个面都有一条法线(normal)时,才会进行灯光的计算。知道了这点,我们来为三角添加法线吧,这样就能在场景里看到它了。最简单的方法就是把顶点格式改为一种包含了法线的格式。碰巧我们也有这样一个结构了,改变创建三角形的代码:
CustomVertex.PositionNormalColored[] verts = new CustomVertex.PositionNormalColored[3];
verts[0].SetPositon(new Vector3(
verts[0].SetNormal(new Vector3(
verts[0].Color = Ststem.Drawing.Color.White.ToArgb();
verts[1]``````
`````````
更新顶点格式来适应新的数据:
device.VertexFormat = CustomVertex.PositionNormalColored.Format;
这次最大的改变就是使用了一组包含法线的数据,并且把三角形的颜色改为白色。可以看到,我们把垂直于顶点指向外的方向定义为法矢量。因为点只是在Z平面内移动,所以沿着Z轴的负方向即是法线矢量的方向。现在程序就一切正常了。可以试着改变一下灯光的漫射颜色,看看会有怎样的变化。
还有一件应该记住的事:灯光是按照每一个顶点来计算,所以在low polygon模型(就像我们简单的三角形)的情况下,灯光可能会不太真实。我们会在后边的章节里讨论一些高级灯光技术,比如per pixel linghting。这些灯光能创造一个真实的世界。
至今为止,示例代码里还有两项没有讨论过:设备状态(device state)以及变换(transform)。对一个设备来说,有三种不同方式的设备状态:the render state,The sampler states,和 the texture state。我们仅仅使用过the render state中的几种类型;后边的两种类型是用来处理纹理的。不要担心我们很快就会谈到纹理。The render state类规定了DirectX3D怎样来对场景进行光栅化。可以使用这个类来改变很多属性,包括我们已经使用过的灯光以及剔除。其他render state可用的选项有填充模式(fill mode) (比如wire frame mode)和各种雾化参数。我们也会来接下来的几章深入讨论。
前面提到过,变换就是用来把几何体位置从一个坐标系转到另一个坐标系的一系列矩阵。用于device上的三个主要变换就是world,view以及projection变换,但是也有一些其他的变换。比如用来控制texture stages的变换,就依赖于一个255的世界矩阵(There are transforms thst are used to modify texture stages,as well as up to 255 world matrices??).
Swapchains and RenderTargets
Device到底作了些什么工作来绘制这些三角形呢?device有一些固定的方法来处理在哪绘制并且如何绘制对象。每一个device都有一个交换链(swap chain)以及一个渲染目标(render target)。
一条交换链实际上就是一系列被控制着用来渲染的缓冲区。所有绘图过程都是在交换链中的后备缓冲区发生。当使用SwapEffect.Flip来创建一条交换链时,后备缓冲区翻转(flipped)为真正被图形卡用于读取数据的前缓冲(front buffer)。同时,三号缓冲区变为新的后备缓冲,而先前的前缓冲变为未使用过的三号缓冲区。
真正的翻转操作是通过改变图形卡当前所读的数据区、刚读过的数据区以及后备缓冲区之间的地址来实现。只有在全屏模式下,才会发生真正的翻转操作。而在窗口模式,翻转实际上只是数据的拷贝而已,因为device并没有控制着整个显示器,仅仅是一小部分而已。虽然两种模式下结果都一样。全屏模式下,有一些驱动程序也会使用翻转操作来实现SwapEffect.Discard 或者 SwapEffect.Copy。
如果使用SwapEffect.Copy或SwapEffect.Flip来创建交换链,可以确保presen()之后不会影响后备缓冲中的内容。运行时会在需要时强制创建额外的隐藏缓冲。建议使用SwapEffect.Discard来避免这种潜在的损失。这种模式允许驱动程序选择最高效的方法分配后备缓冲。使用SwapEffect.Discard时,不值得(???)在绘制新的图形前检查你是否清除了整个后备缓冲。调试模式下的运行时将会使用随机的数据来填充(刚刚使用过的)后备缓冲,让开发者检查是否忘了调用clear()。(it is worth nothing that when usuing SwapEffect.Discardyou will want to ensure that you clear the entire back buffer before starting new drawing operations. the runtime will fill the the back buffer with random data in the debug runtime so developers can see if they forget to call clear)(注:这一段内容看的不是太明白,所以把原文也给出来。Sdk中对SwapEffect枚举的解释也不是太清除。参考sdk:交换效果明确定义了调用present()之后,后备缓冲的状态。Flip交换链是一个循环的队列,可以有0~(n-1)块后备缓冲, discard交换链是一个队列, copy交换链只有一块后备缓冲。Flip中的后备缓冲在present()之后内容不会改变,所以系统需要额外内存作为后备缓冲,带来性能损失。既然后备缓冲中的内容不改变,如何构成循环队列来使用?? Discard后备缓冲中队列的长度以及怎样变化也没有明确说明,只有“The swap chain is essentially a queue where 0 always indexes the back buffer that will be displayed by the next Present operation and from which buffers are discarded once they have been displayed. An application that uses this swap effect should update an entire back buffer before invoking a Present operation that displays it.The debug version of the runtime overwrites the contents of discarded back buffers with random data, to enable developers to verify that their applications are updating the entire back buffer surface correctly.” 随机数据能帮助检查是否更新了整个后备缓冲区??既然会丢弃数据还需要调用clear??)
交换链的后备缓冲区也同样能作为渲染目标。毫无疑问,当创建了device,创建了交换链之后,渲染目标就被设置为链的后备缓冲。一个渲染目标就是能保存所执行的绘制任务的输出的表面(a surface that will hold the output of the drawing operations that you perform)。如果你创建了多个交换链的话,就必须确定预先更新了device的渲染目标。后边我们会稍后讨论这点。