分类: C/C++
2008-02-20 12:46:39
使用简单的渲染技术
翻译: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(
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(
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(
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(
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*
device.DrawPrimitives(PrimitiveType.TriangleList,0,12);
这里最大的改变就是重新定义了顶点缓冲的大小。同时,我们也改变了盒子的旋转角度,让他转的更疯狂一点。最后改变所要渲染的图元数量。实际上,既然盒子完全是三维的,就没有必要看到他的背面。使用Direct3D里的默认剔除模式(逆时针):删除前面申明剔除模式的行。好了现在运行程序。
非常了不起,我们现在有了一个在屏幕中疯狂旋转的彩色盒子。但是如果需要渲染一系列盒子的话,没有人希望申明一系列顶点缓冲吧。有一个简单的方法可以做到这一点。
现在我们要肩并肩的绘制三个盒子。由于现在的摄像机设置让第一个盒子占慢了整个屏幕,我们需要把他摄像机稍稍往后移一点:
device.Transform.View = Matrix.LookAtLH(new Vector3(0,0,
如你所见,我们只是把他往后移了一点点就可以看到更多场景。为了绘制更多的盒子,我们可以再次利用现有的顶点缓冲,只需要告诉Direct3D再次绘制同样的顶点就可以了。在device.DrawPrimitives之后添加一下代码:
device.Transform.World = Matrix.RotationYawPitchRoll(angle/(float)Math.PI, angle/(float)Math.PI/
device.DrawPrimitives(PrimitiveType.TriangleList,0,12);
device.Transform.World = Matrix.RotationYawPitchRoll(angle/(float)Math.PI, angle/(float)Math.PI*
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(
vert[1]```````````(略)
显然,最大的改变就是储存顶点集合的数据类型。每个顶点中的最后两个float值储存了渲染图元所用的纹理U、V值。应为盒子每个面和纹理都为正方形,所以直接把纹理映射到每个面就可以了。注意,图元的左上角映射纹理的(0,0)textl,右下角映射到(1,1)textl。同时,我们还必须修改创建顶点缓冲的地方:
vb = new VertexBuffer(typeof(CustomVertex.PositionTextured),36,device,Usage.Dynamic|Usage.WriteOnly, CustomVertex.PositionTextured.Format,Pool.Default);
有如此多的重复代码,现在让我们用一个简单的方法来绘制盒子,添加一个函数完成这个任务。
private void DrawBox(float yaw, float potch, float roll, float x, float y,float z,texture t)
{
ngle +=
device.Transform.World = Matrix.RotationYawPitchRoll(yaw,pitch,roll)*Matrix.Translation(x,y,z);
device.SetTexture(0,t);
device.DrawPrimitives(PrimitiveType.TriangleList,0,12);
}
前六个参数和我们之前使用的一样,最后一个新的参数表示渲染时所使用的纹理。我们还调用了SetTexture方告诉Direct3D渲染时使用哪个纹理。它的第一个参数是这张纹理的“层(stage)”。还记得先前我提过可以为一个图元渲染8层纹理吗,这个参数就是这些纹理的索引。因为只有一张纹理,我们使用第一个索引,0。同时应该注意到,我们修改了angle变量以及world transform,可以把SetupCamera里同样的几行删了。
在调用新方法渲染之前,先要申明一些将要使用的纹理,源码里附带了一个包含三张纹理的资源文件。分别为puck.bmp,ground.bmp,banana.bmp。添加如下代码:
private Texture
private Texture tex1 = null;
private Texture tex2 = null;
这是我们即将使用的三张纹理。但是,还需要真正“装配”起作为资源嵌入的三张位图。在创建顶点缓冲之后添加如下代码:
tex1 =·····(略)
Texture的构造函数接受四个参数。第一个是用于渲染纹理的device。场景里所有的资源(纹理,顶点缓冲,等等)都要和device发生联系。下一个参数Bitmap是我们获取纹理数据的地方。第三个参数Usage,先前已经讨论过它。最后一个参数是储存纹理的内存池位置。方便起见,现在使用托管的内存池。Texture其他的构造函数包括:
(注:此处略去一个在DX
public Texture( Device device, int width, int height, int numLevels,Usage usage, Format format, Pool pool);
public Texture( Device device, Stream data, Usage usage,Pool pool);
第一个方法允许我们从“空白”开始创建一张纹理,可以指定它的高度、宽度,细节程度(number of levels of detail)。最后一个和我们使用的很相似,但使用流而不是位图对象。当然,流中的数据要能被转换为位图。TextureLoad类中还有一些关于加载位图的有趣方法,我们将在下一张讨论。
好了,现在已经定义且加载了位图,是更新绘图代码的时候了,使用如下代码代替先前的绘图代码;
DrawBox(angle / (float)Math.PI, angle / (float)Math.PI *
DrawBox(angle / (float)Math.PI, angle / (float)Math.PI /
DrawBox(angle / (float)Math.PI, angle / (float)Math.PI *