本文由哈利_蜘蛛侠原创,转载请注明出处!有问题请联系[email protected]
这一次我们继续来讲述Jim Adams老哥的RPG编程书籍第二版第二章的第4节:Getting Down to Drawing。这一节可以说是超级长了,所以我们就分3次来讲吧!
由于这一节的内容实在是太多,所以我这一节的各小节的标题列在下面,以供大家参考:
1、Using Vertices (使用顶点)
2、Flexible Vertex Format (灵活顶点格式)
3、Using Vertex Buffers (使用顶点缓存)
4、Vertex Streams (顶点流)
5、Vertex Shaders (顶点着色器)
6、Transformations(变换)
7、The World Transformation (世界变换)
8、The View Transformation (视角变换)
9、The Projection Transformation (投影变换)
10、Materials and Colors (材质和颜色)
11、Clearing the Viewport (清除视口)
12、Beginning and Ending a Scene (开始和结束场景)
13、Rendering Polygons (渲染多边形)
14、Presenting the Scene (展示场景)
这一期要从Using Vertices讲到Vertex Shaders。
原文翻译:
===============================================================================
基础部分讲的足够多了;是时候来看看Direct3D到底是怎么绘制图形的。在这一节中,我会介绍使用顶点和多边形来绘制图形的基本知识。你会学到Direct3D使用顶点来绘制多边形的各种方式,如何给这些多边形上色,并最终向用户展示这些图形。正所谓细节决定成败,因此要好好研究处理顶点的方法,再从这里继续前进。
Direct3D给了你很多不同的方式来定义一个顶点。例如,如果你在使用2-D图形,你可以在2-D屏幕坐标(变换后的坐标)中设定坐标。
另一方面,如果你在使用局部的或者世界的空间坐标,你可以在3-D坐标(未变换的坐标)中设定坐标。那么如何使用颜色和纹理呢?你可以在你的顶点定义中选择包含进这些信息。
那么你如何跟踪所有的这些信息并且保证Direct3D直到你正在做什么呢?请看灵活顶点格式。
灵活顶点格式(flexible vertex format,简称FVF)用于构造自定义的顶点数据以便在你的应用程序中使用。使用FVF,你必须决定你的顶点要使用哪些信息——例如3-D坐标、2-D坐标、颜色等等。
你使用一种标准的结构体来创建FVF,在这个结构体中你只加入你想要的成分。当然有一些限制,例如你必须用某种特定的顺序来列举这些成分,并且某些成分不能与其他的成分相冲突(例如同时使用2-D坐标和3-D坐标)。一旦这个结构体完成后,你建立一个FVF描述器(FVF descriptor),它是描述你的顶点格式的一系列标记的组合。
下面的代码块包含了一个使用了各种在FVF中允许使用的变量(或者说,至少是所有的我在这本书中用到的变量)的顶点结构体。结构体中的变量列举的顺序与它们在你自己的结构体中出现的顺序严格保持一致;如果你截去了任何变量,保证剩下的保持顺序:
typedef struct { FLOAT x,y, z, rhw; // 2-D coordinates FLOAT x,y, z; // 3-D coordinates FLOAT nx,ny, nz; // Normals D3DCOLOR <span style="white-space:pre"> </span> diffuse; // Diffuse color FLOAT u,v; // Texturecoordinates } sVertex;
正如你所看到的,唯一的互相冲突的变量是表示坐标的变量,包括法向量(normals)。法向量是用来定义一个方向的并且只能与3-D坐标联合使用的坐标。你需要选择哪些坐标(要么2-D,要么3-D)要保留,哪些坐标要丢弃。如果你在使用2-D坐标,那么你就不能包含进3-D坐标;反之亦然。
2-D坐标和3-D坐标的唯一的真正的区别是额外的rhw变量,它是其次坐标(homogeneous)W的倒数。用通俗的话来说(原文是In English),这一般代表从观察点到顶点的沿着z-轴的距离。在大多数情况下,你可以安全地将rhw值设为1.0。
还要注意,sVertex坐标使用了数据类型FLOAT(它是一个浮点值),但是D3DCOLOR是啥数据类型呢?D3DCOLOR是一个DWORD值,在Direct3D中,你用之来储存颜色值。为了构造一个颜色值用于D3DCOLOR,你可以从两个函数中进行选择:D3DCOLOR_RGBA或者D3DCOLOR_COLORVALUE:
D3DCOLORD3DCOLOR_RGBA(Red, Green, Blue, Alpha); D3DCOLOR D3DCOLOR_COLORVALUE(Red,Green, Blue, Alpha);
每个函数(事实上它们是宏)取四个参数,代表着要用到的各个颜色成分的量,包括一个alpha值(透明度)。这些值在D3DCOLOR_RGBA中可以从0变到255,而在D3DCOLOR_COLORVALUE中可以从0.0变到1.0(分数)。如果你在使用实心的颜色(不透明的颜色),那么就把alpha值恒取为255(或1.0)。
作为例子,比如说你只想在你的顶点结构中包含进3-D坐标和一个漫反射颜色成分:
typedef struct { FLOAT x,y, z; D3DCOLOR diffuse; } sVertex;
创建你的FVF的下一步是使用表格2.3中列出来的任意标记组合来建立FVF描述器。
表2.3 灵活顶点格式描述器标记
Flag |
Description |
D3DFVF_XYZ |
包含进了3-D坐标。 |
D3DFVF_XYZRHW |
包含进了2-D坐标。 |
D3DFVF_NORMAL |
包含了法向量(一个向量)。 |
D3DFV_DIFFUSE |
包含进了一个漫反射颜色成分。 |
D3DFVF_TEX1 |
包含进了一组纹理坐标。 |
为了描述一个FVF描述器,你把所有适当的标记组合进一个define语句中(假设你在使用3-D坐标和漫反射颜色成分):
#define VertexFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE)
只需要保证所有的标记与你加入到你的顶点结构体中的成分相匹配,那么一切就会顺利的。
在你构造你的顶点结构体以及描述器之后,你要创建一个包含了一个顶点数组的对象。Direct3D给了你两个对象来使用:IDirect3DVertexBuffer9和IDirect3DIndexBuffer9。我在这本书中使用的对象是IDiret3DVertexBuffer9,它储存着用于绘制三角形列、三角形带和三角形扇的顶点。(实际上,将它与IDirect3DIndexBuffer9结合起来使用才是比较省效率的方法;这也正是本人在自己的更新版代码中使用的方法。)
当用于三角形列时,IDirect3DVertexBuffer9对象为每个要绘制的多边形储存至少3个顶点(这些顶点以顺时针顺序排序)。对于三角形带,第一个将被绘制的多边形使用3个顶点,而后面的每个要被绘制的多边形只需要使用1个额外的顶点。对于三角形扇,储存有一个中心的顶点,而每一个将被绘制的多边形要储存两个额外的顶点(其实一般来说只要一个额外的顶点,见下面的正方形例子)。
注意:
===============================================================================
一个多边形可以使用1个、2个或3个顶点,这取决于你在绘制什么图形。像素只需要一个单个的顶点,线段需要两个,而三角形多边形需要三个。在这本书中,我主要处理的是三角形的多边形。
===============================================================================
图2.11应该可以帮助你更好地理解你如何使用储存的顶点以及你以什么顺序布置这些顶点。在图中,有一个可以用三种方法之一定义的正方形。在第一种方法中,是使用一个三角形列,所以你需要使用6个顶点来定义这个正方形——两个三角形的每一个都要花去三个顶点。
第二种方法是使用一个三角形带。三角形带只使用4个顶点,这在图中表示得很明白。前三个顶点构造了第一个面,而最后一个多边形定义了第二个面。对于第三种方法,也就是三角形扇方法,你还是使用4个顶点。但是,用三角形扇时,你定义的第一个顶点变成了扇的基点,而其余的顶点定义了扇面。
你通过初始化后的IDirect3DDevice9对象来创建一个顶点缓存:
HRESULT IDirect3DDevice9::CreateVertexBuffer( UINT Length, // # of bytes to use, in multiples //of vertex structure size. DWORD Usage, <span style="white-space:pre"> </span> // 0 DWORD FVF, // FVF descriptor D3DPOOL Pool, // D3DPOOL_MANAGED IDirect3DVertexBuffer9 **ppVertexBuffer, // the vertex buffer HANDLE *pHandle); //set to NULL
在CreateVertexBuffer函数的调用中你唯一想要改变的参数是Usage标记,它告诉Direct3D如何对待用于储存顶点数据的内存。为了呆在安全的一边,你应该始终将Usage设为0,但是如果你想提升一点性能的话,将Usage设为D3DCREATE_WRITEONLY。这样做会告诉Direct3D你不准备读取顶点数据,然后Direct3D会适当地储存顶点数据。一般来说,这意味着顶点数据会储存在图形硬件的内存中(称为:快速读取内存,faster-access memory)。
这是一个简短的例子(建立在我之前——在“灵活顶点格式”这一节中——的顶点格式基础上,它只使用了3-D坐标和漫反射顶点成分),它创建了一个包含4个顶点的顶点缓存:
// g_PD3DDevice = pre– initialized device object // sVertex =pre-defined vertex structure // VertexFVF =pre-defined Vertex FVF descriptor IDirect3DVertexBuffer9*PD3DVB = NULL; // Create the vertexbuffer if(FAILED(g_PD3DDevice->CreateVertexBuffer( \ sizeof(sVertex) * 4, D3DCREATE_WRITEONLY, VertexFVF, \ D3DPOOL_MANAGED, &PD3DVB, NULL))) { // Error occurred }
注意:
===============================================================================
在创建你自己的顶点缓存时,确保你设定了合适的缓存大小(CreateVertexBuffer函数调用的第一个参数)。在这里展示的例子中,你开辟了足够的内存来储存4个sVertex格式的顶点。这里的4表示4个实例,而sVertex是你用于储存你的顶点数据的结构体。
===============================================================================
注意:
===============================================================================
跟以前一样,在你用完顶点缓存后,确保通过调用其Release函数来释放这个COM对象。
===============================================================================
在你能够将顶点添加进顶点缓存对象之前,你必须锁定该缓存所使用的内存。这保证存储顶点的内存是位于可利用的内存区域。然后你使用一个内存指针来访问顶点缓存内存。你通过调用缓存对象的Lock函数来锁定顶点缓存的内存并返回一个内存指针:
HRESULTIDirect3DVertexBuffer9::Lock( UINT OffsetToLock, // offset to lock buffer, in bytes UINT SizeToLock, // how many bytes to lock, 0 = all VOID **ppbData, // pointer to a pointer (to access data) DWORD Flags); //0
在这里,你让offset位于缓存中你想要访问的位置处(以字节数来计算),并且指定你想要访问的字节数目(0代表全部)。然后你需要做的全部就是向函数传递指向你将用来访问顶点缓存的内存指针的指针(转型成为一个VOID数据类型)。下面是一个锁定整个顶点缓存的调用的示例:
// pD3DVB =pre-intialized vertex buffer object BYTE *Ptr; // Lock the vertexbuffer memory and get a pointer to it if(FAILED(pD3DVB->Lock(0,0, (void**)&Ptr, 0))) { // Error }
注意:
===============================================================================
在其创建时使用D3DCREATE_WRITEONLY标记的顶点缓存无法进行读取,只能够写入。如果你想从一个顶点缓存读取数据(就像D3DX中某些函数那样),你应该在你调用CreateVertexBuffer函数时将Usage参数设为0。
===============================================================================
在你结束了对顶点缓存的访问之后,始终要记得在每个Lock函数的调用之后跟上一个IDirect3DVertexBuffer9::Unlock函数的调用:
HRESULTIDirect3DVertexBuffer9::Unlock();
解锁一个顶点缓存确保Direct3D可以开始安全地使用它了,因为它知道你不会再修改该缓存内的任何数据了。
当心!
===============================================================================
确保最小化在调用Lock函数和调用Unlock函数之间的时间量。你处理被锁定的顶点缓存越快则越好,因为Direct3D必须停下来等你弄完顶点缓存,以便能够访问其内部包含的数据。
===============================================================================
现在你有了你的顶点结构体,描述器以及缓存,并且你已经锁定了缓存并准备储存顶点数据。因为你已经从Lock函数的调用中得到了指向顶点缓存内存的数据指针,你所需要做的就是将适当数量的顶点复制进顶点缓存。
继续我的例子,并利用我已经定义的顶点格式(使用3-D格式和漫反射颜色成分),我在一个数组内部创建了一个局部的顶点数据集合:
sVertex Verts[4] = { { -100.0f, 100.0f, 100.0f, D3DCOLOR_RGBA(255, 255,255, 255) }, { 100.0f, 100.0f, 100.0f, D3DCOLOR_RGBA(255, 0, 0,255) }, { 100.0f, -100.0f, 100.0f, D3DCOLOR_RGBA(0, 255, 0,255) }, { -100.0f, -100.0f, 100.0f, D3DCOLOR_RGBA(0, 0, 255, 255) } };
锁定顶点缓存,这样得到一个指向顶点缓存内存的指针,然后复制这个局部的顶点数据(并且在弄完之后解锁顶点缓存):
// pD3DVB =pre-initialized vertex buffer object BYTE *Ptr; // Lock the vertexbuffer memory and get a pointer to it if(SUCCEEDED(pD3DVB->Lock(0,0, (void**)&Ptr, 0))) { // Copy local vertices into vertex buffer memcpy(Ptr, Verts, sizeof(Verts)); // Unlock the vertex buffer pD3DVB->Unlock(); }
这就是构建一个顶点缓存并往里填充顶点数据要做的所有事情!要使用顶点信息,现在你只需要指定一个流源(stream source)和顶点渲染器(vertex shader)。
Direct3D让你能够通过一系列不同的流(stream)——称为顶点流(vertex streams)——将顶点喂给(feed to)渲染器(renderer)。你可以通过将多个流的顶点数据整合到一个流中来创造非常令人印象深刻的结果,但是在这本书中,我只是用一个流,因为使用多个流的复杂性已经超出本书范围了。
为了将你的顶点数据复制到一个流中,你使用IDirect3DDevice9::SetStreamSource函数:
HRESULTIDirect3DDevice9::SetStreamSource( UINT StreamNumber, // 0 IDirect3DVertexBuffer9* pStreamData, // Vertex buffer object UINT OffsetInBytes, // Offset (in bytes) of vertex data //in the vertex buffer. UINT Stride); // Size of vertexstructure
你现在设定顶点流源(vertex stream source)要做的所有事情就是调用这个函数,并传递指向顶点缓存对象的指针、提供用于存储顶点结构体的字节数(使用sizeof)。
如果你在顶点缓存中存储着多余一组多边形(比如几个三角形带),你可以将OffsetInBytes参数设为顶点数据的offset。例如,如果我使用顶点缓存来储存两个三角形带(第一个三角形带位于offset 0处,而第二个三角形带位于offset 6处),那么我通过设置适当的offset(在这种情况下,第二个三角形带的offset为6)来绘制第二个三角形带。
从上一节中在顶点缓存中储存顶点的例子出发,你可以使用下面的方法:
// g_pD3DDevice –pre-initialized device object // pD3DVB =pre-initialized vertex buffer if(FAILED(g_pD3DDevice->SetStreamSource(0,pD3DVB, \ 0, sizeof(sVertex)))) { // Error occurred }
作为使用顶点来绘制图形的最后一步,你需要理解顶点着色器的概念。顶点着色器(vertex shader)是这样一种机制,它操控顶点的载入和处理;这具体包括改变顶点坐标、运用颜色和雾化功能以及很多其他的顶点组件。
一个顶点着色器可以以两种形式出现。它可以是固定的(fixed)顶点着色器(其中所有的用于完成一般功能的函数都已经是内建的了),也可以是可编程的(programmable)顶点着色器(其中你可以在渲染到显示屏之前自定义函数来改变顶点信息)。
尝试去解释可编程顶点着色器超出了本书的范围。(然而,本人在更新版的代码中一直使用的是用HLSL语言写的可编程顶点着色器。)相反,我集中于使用固定的顶点着色器,因为它们包含了你所需要的所有的功能。(未必;在十年前你可能认为足够了,但是对于新一代的玩过很多酷炫游戏的我们来说,这是远远不够的。)
为了在你的顶点上使用固定顶点着色器,你将你的自定义顶点的FVF描述器传递给IDirect3DDevice9::SetFVF函数中:
HRESULTIDirect3DDevice9::SetFVF( DWORD FVF); // Vertex FVF descriptor
使用前面的函数跟下面一样简单:
// g_pD3DDevice =pre-initialized device object // VertexFVF =pre-defined vertex FVF descriptor if(FAILED(g_pD3DDevice->SetFVF(VertexFVF))){ // Error occurred }
你现在已经建立了顶点信息了。下一个步骤是建立将顶点(在局部空间中)放置到其世界空间坐标时所需要的各种各样的变换。当然,只有你在使用3-D坐标的时候才需要这一步。
===============================================================================
好了,这几个小节的内容总算讲完了。可能读者看得不是太明白,尤其是那些函数的具体参数的意义之类的。其实这个大家可以参考SDK文档,或者参考龙书第二版。另外这里所用的设置顶点的方法在本人的更新版代码中是不会使用的,而是用的是龙书第二版中更加先进、更加灵活的方式,所以看得不太懂也没什么关系。