【翻译】managed DirectX (第三章)

** 使用简单的渲染技术


翻译:clayman

至今为止,我们的渲染工作效率都很低。每次渲染场景时,都要分配新的顶点列表,并且所有东西存储在系统内存里。现代显卡集成了足够显存,把顶点数据存放在显存可以获得大幅的新能提升:存放在系统内存里的数据,渲染每一帧时都要拷贝到显卡,这会带来极大的损失。只有移除每帧时的这种分配才能帮助我们提高性能。

** 使用顶点缓冲( ** ** Using Vertex Buffers ** ** ) ** ** **

Direct3D 已经包含了这种机制:顶点缓冲( vertex buffer )。顶点缓冲,就像他名字的意思一样:一块储存顶点的内存。顶点缓冲的机动性能完美实现共享场景里变经过变换的几何体。如何让我们在第一章编写的三角形程序使用顶点缓冲呢?

创建顶点缓冲同样简单,有三个构造函数能完成这个任务,我们依次来看看:

** public VertexBuffer( Device device, int sizeOfBufferInBytes, Usage usage, VertexFormats vertexFormat, Pool pool); **

** public VertexBuffer( Type typeVertexType, int numVerts, Device device, Usage usage,VertexFormats vertexFormat, Pool pool); **

以下是各参数的意义:

n device ——用来创建顶点缓冲的 device ,创建的顶点缓冲只能被这个 device 使用;

n sizeOfBufferInBytes ——所创建的顶点缓冲大小,以字节为单位。使用带有这个参数的构造函数创建的顶点缓冲可以存放任何类型的顶点;

n typeVertexType ——如果去要创建的顶点缓冲只储存一种类型的顶点,则使用这个参数。它的值可以是 CustomVertex 类中的顶点结构类型,也可以是自定义的顶点类型。且这个值不能为 null ;

n numVert ——指定了顶点缓冲的储存类型之后,也必须指定缓冲储存的顶点数量最大值。这个值必须大于 0 ;

n usage ——定义如何使用顶点缓冲。并不会是所有 Usage 类型的成员都能使用,只有一下几个是正确的参数:

DoNotClip,Dynamic, Npatches, Points, PTPatches, SoftwareProcessing, WriteOnly;

n vertexFormat —— 定义储存在顶点缓冲中的顶点格式。,如果创建的为通用缓冲的话,则使用 VertexFormat.None ;

n pool ——定位顶点缓冲使用的内存池位置,可以指定一下几个内存池位置:

Default , Managed , SystemMemory , Scratch 。

观察第一章中的程序,把三角形的数据移动到顶点缓冲里应该很容易。首先,申明顶点缓冲变量:

** private Device device = null; **

** private VertexBuffer vb = null; **

接着添加创建三角形的代码:

** device = new (0,DeviceType.Hardware, this.CreatFlags.softwreVertexProccessing, presentParams); **

** 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]````````` **

** vb = new VertexBuffer(typeof(VustomVertex.PositionColored),2,device,Usage.Dynamic| Usage.WriteOnly, CustomVertex.PositionColored.Format, Pllo.Default); **

** vb.SetData(vets,0,LockFlags.None); **


** ** 唯一的改变就是定义了三角形之后的两行代码。首先,创建用来保存三个顶点的顶点缓冲。出于性能上的考虑,创建的缓冲是动态、只读的并且位于默认的内存池。接下来,我们把三角形的顶点放到缓冲内,使用简单的 SetData 方法。这个方法接收任何类型的对象作为第一个参数,第二个参数是顶点缓冲中所要放置数据地址的便宜量。我们打算填充所有的顶点缓冲,所以设置为 0 。最后一个参数描述了当写入数据时,如何锁定缓冲。我们将稍后讨论锁存机制;现在,不用关心他是怎样锁定的。

现在编译程序,很自然,得到了一个编译错误:因为 OnPaint 方法里的 DrawUserPrimitives 需要获得 verts 变量。需要有一个方法告诉 Direct3D ,我们要绘制顶点缓冲里的内容,而不是先前所申明的数组。调用 device 的 SetStreamSource 让 Direct3D 绘图的时候读取顶点缓冲。这个方法有以下两种重载:

**public void SetStreamSource(int streamNumber, VertexBuffer streamData, int offsetInBytes, int stride); **

** public void SetStreamSource( int streamNumber, VertexBuffer streamData, int offsetInBytes); **


两个函数的不同之处在于其中一个多了表示(数据)流步幅大小( stride size of the stream )的参数。第一个参数是这段数据所使用流的数量。现在,把它设置为 0 即可;我们会在下一章讨论使用多个流。第二个参数是作为数据源的顶点缓冲,第三个则是顶点缓冲里需要 DirectX 绘制的数据的偏移量(以字节为单位)。 stride 则是缓冲里每一个顶点的大小。如果是用特定类型创建的顶点缓冲,则不需要这个参数。

现在修改绘图的方法:

** device.SetStreamSource(0, vb, 0); **

** device.DrawPrimitives(PrimitiveType.TriangleLise, 0, 1); **


正如刚才所描述的,我们把顶点缓冲作为数据流 0 ,同时把偏移量设置为 0 ,使用所有数据。值得注意的是,我们同时也改变了真正绘图的函数。既然所有数据都在顶点缓冲里了,就不需要调用 DrawUserPrimitives 方法。因为 DrawUserPrimitives 只是用于绘制直接传递给它的用户定义数据。更加通用的 DrawPrimitives 将会绘制来自数据流源里的几何体。 DrawPrimitives 有三个参数,第一个我们已经讨论过了。第二个表示流里的起始顶点,最后一个表示所要绘制的几何体个数。

就连这个仅绘制一个三角形的小样在使用了顶点缓冲之后都带来了 10 %的性能提升(基于画面更新率,即帧频 frame rate )。我们会在稍后几张来讨论有关性能及帧频。不幸的是,当你尝试改变窗口大小的时候,三角形会立即消失。(注:偶在实际测试时三角形并米有消失,只是当窗口缩放为一定比例时,三角形会消失)

有几种情况会导致这种行为,其中的两种我们先前已经讨论过了。回想一下上一章,我们知道在改变窗口大小的时候,设备会自动重置。但当所创建的资源位于默认的内存池时(比如顶点缓冲),重置设备会释缓冲。所以当改变窗口大小的时候,重置了 device ,释放了顶点缓冲。 Managed DirectX 有一个极好的特新就是在重置 device 之后会自动的重建顶点缓冲。但是,这是顶点缓冲里已经没有了数据,所以没有任何东西被绘制出来。

我们可以捕获顶点缓冲一个叫做“ created ”的事件,它会在重建顶点缓冲,准备好填充数据的时候发生。现在是使用这个事件更新我们程序的时候了,修改代码如下:

** private void OnVertexBufferCreate(object sender, EventArgs e) **

** { **

** VertexBuffer buffer = (VertexBuffer)sender; **

** 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]````````` **

** buffer.SetData(verts,0,LockFlags.None); **

** } **

** ** 订阅事件处理程序:

vb.Created += new EventHandleer(this.OnVertexBufferCreate);

OnVertexBufferCreate(vb,null);

这段代码为顶点缓冲订阅了事件处理程序,并且保证无论在什么情况下创建顶点缓冲,都会调用 OnVertexBufferCreate 方法。因为第一次创建顶点缓冲的时候,还没有订阅过处理程序,所以需要手动调用一次。

好了,通过使用 video memory 和顶点缓冲,我们已经把原来缓慢的小样改变为了一个高效的程序。当然,它还是相当的枯燥。那么,接下来让我们创造一个盒子吧。

三维场景里的所有几何体都是由三角形组成,那么如何来渲染一个盒子或一个立方体呢? Well ,每个立方体由六个正方形构成,而两个三角形可以构成一个正方形(呵呵,这个都要讲,看来老外的数学真的不行)实际上,我们只需要获得立方体 8 个顶点的坐标就可以了。添加代码:

** CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[36]; **

** // Front face **

** verts[0] = new CustomVertex.PositionColored( -1.0f , 1.0f , 1.0f , Color.Red.ToArgb()); **

** verts[2] =````, verts[3] , verts[4], verts[5] =````````` **

** // Back face (remember this is facing away from the camera, so vertices should be clockwise order) **

** verts[6] = new CustomVertex.PositionColored( -1.0f , 1.0f , -1.0f , Color.Blue.ToArgb()); **

** verts[7] , verts[8], verts[9], verts[10], verts[11]=``````` ** `

( 注:详见附件中的源码,注意顶点申明的顺序 )

正如前面提到的,盒子由 12 个三角形组成,每个三角形有三个顶点,构成一个顶点集合。还有几个需要修改的地方

** vb = new VertexBuffer(typeof(CustomVertex.PositionColored),36,device,Usage.Dynamic | Usage.WriteOnly,CustomVertex.PositionColored.Format,Pool.Default); **

** evice.Transform.World = Matrix.RotationYawPitchRoll(angle/(float)Math.PI, angle/(float)Math.PI* 2.0f , angle/(float)Math.PI); **

** device.DrawPrimitives(PrimitiveType.TriangleList,0,12); **

这里最大的改变就是重新定义了顶点缓冲的大小。同时,我们也改变了盒子的旋转角度,让他转的更疯狂一点。最后改变所要渲染的图元数量。实际上,既然盒子完全是三维的,就没有必要看到他的背面。使用 Direct3D 里的默认剔除模式(逆时针):删除前面申明剔除模式的行。好了现在运行程序。

非常了不起,我们现在有了一个在屏幕中疯狂旋转的彩色盒子。但是如果需要渲染一系列盒子的话,没有人希望申明一系列顶点缓冲吧。有一个简单的方法可以做到这一点。

现在我们要肩并肩的绘制三个盒子。由于现在的摄像机设置让第一个盒子占慢了整个屏幕,我们需要把他摄像机稍稍往后移一点:

** device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, 18.0f ),new Vector3(),new Vector3(0,1,0)); **

如你所见,我们只是把他往后移了一点点就可以看到更多场景。为了绘制更多的盒子,我们可以再次利用现有的顶点缓冲,只需要告诉 Direct3D 再次绘制同样的顶点就可以了。在 device.DrawPrimitives 之后添加一下代码:

** device.Transform.World = Matrix.RotationYawPitchRoll(angle/(float)Math.PI, angle/(float)Math.PI/ 2.0f , angle/(float)Math.PI* 4.0f ) *Matrix.Translation( 5.0f , 0.0f , 0.0f ); **

** device.DrawPrimitives(PrimitiveType.TriangleList,0,12); **

** device.Transform.World = Matrix.RotationYawPitchRoll(angle/(float)Math.PI, angle/(float)Math.PI* 4.0f , angle/(float)Math.PI/ 2.0f )*Matrix.Translation( -5.0f , 0.0f , 0.0f ); **

** device.DrawPrimitives(PrimitiveType.TriangleList,0,12); **

好了,这次我们又作了些什么呢?因为绘制第一个盒子时已经设置过 VertexFormat 属性,所以 Direct3D 知道将要绘制的顶点类型。同样,它也知道在哪里获得数据。那么绘制第二个盒子 Direct3D 还需要知道什么呢?只需要绘制的位置和绘制什么就可以了。

设置 world transform 可以把数据从局部坐标( object space )“移动”到世界坐标( world space ),那么把什么用作变换矩阵呢?首先,使用类似 SetupCamera 函数里的方法;做一点点改变,让盒子以不同的角度旋转。然而 world transform 里的另一半则是新内容:把一个 Matrix.Translation 与现有的旋转矩阵相乘。变换矩阵可以把空间中的一个点移动到另一个位置。我们的变换矩阵把第二个盒子向坐移动了 5 个单位,第三个盒子则向右移动了 5 个单位。

需要注意的是两个变换矩阵相乘得到的累积效果,是由相乘时矩阵的顺序来决定的。在这里,我们的先旋转盒子,然后再移动。如果先移动再选旋转,那么结果将有很大区别。记住变换时的顺序是很重要的。

** 为对象添加纹理 **

** ** 虽然使用颜色和灯光来渲染很有趣,但仅使用这样的技术,对象看起来并不真实。在非三维的程序里“纹理( texture )”通常用来描述对象的粗糙程度( roughness of an object )。三维场景里的纹理就是一张用来模拟几何图元纹理的 2D 位图。 Direct3D 可以同时为每一个图元渲染 8 层纹理,但现在,我们只解决每个图原一张纹理的情况。因为 Direct3D 使用普通的位图作为它的纹理格式,任何加载的位图都能当作纹理对象。 如何把 2D 的纹理映射到 3D 的对象上呢?绘制到场景中的每个对象都有一个可以在光栅化时把每个 texel 映射到屏幕特定位置的纹理坐标。 textl 是 texture element 的缩写,或者表示纹理中每个 address 的特定颜色值。 Address 可以想象为一个表示行和列的数字,分别称为 U , V 坐标。一般来说,这些值都是标量,取值范围从 0.0 到 1.0 。( 0 , 0 )表示纹理的左上角,( 1,1 )表示右下角,中央的坐标为( 0.5,0.5 )。

为了使用纹理来渲染盒子,必须改变盒子的顶点格式,以及传递给图形卡的数据。使用纹理坐标来代替顶点数据中的“ color ”元素。虽然同时使用颜色和纹理都是有效的,当作为练习,我们只用纹理来定义图元的颜色。修改代码:

**CustomVertex.PositionTextured[] verts = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured[36]; **

** verts[0] = new CustomVertex.PositionTextured( -1.0f , 1.0f , 1.0f , 0.0f , 0.0f ); **

** vert[1]```````````( ** ** 略 ) **

显然,最大的改变就是储存顶点集合的数据类型。每个顶点中的最后两个 float 值储存了渲染图元所用的纹理 U 、 V 值。应为盒子每个面和纹理都为正方形,所以直接把纹理映射到每个面就可以了。注意,图元的左上角映射纹理的( 0 , 0 ) textl ,右下角映射到( 1 , 1 ) textl 。同时,我们还必须修改创建顶点缓冲的地方:

** <SPAN lang=EN- **

Published At
Categories with Web编程
Tagged with
comments powered by Disqus