本文由哈利_蜘蛛侠原创,转载请注明出处!有问题请联系[email protected]
这一次我们继续来讲述Jim Adams 老哥的RPG编程书籍第二版第二章的第5节:Using Texture Maps,也就是使用纹理映射。这一节的内容不多,所以就一次性讲完吧!
我们先将这一节的各小节的标题列在下面,以供大家参考:
1、 Using Texture Mapping with Direct3D (在Direct3D中使用纹理映射)
2、 Loading a Texture (载入一个纹理)
3、 Setting the Texture (设置纹理)
4、 UsingSamplers (使用取样器)
5、 Rendering Textured Objects (渲染带纹理的物体)
原文翻译:
===============================================================================
虽然你已经学会了将3-D物体绘制到显示屏的方法,但是单色的多边形未免太索然无味了。是时候加点料,增加一些细节了。增加3-D物体的细节的最简单的方式之一就是使用一种称为纹理映射的技术。纹理映射(texture mapping)是这样的一种技术,你使用它来通过一张图片给多边形面涂色(如图2.14所示),这样就提高了被渲染物体的视觉表现。位图通常被称为纹理(texture),所以我在讨论3-D渲染的时候会交替使用两个术语。(当然,像我之前说过的那样,纹理图形基本上可以是任何格式的图片。)
使用纹理映射的时候,你要赋给多边形每一个顶点一对坐标。这些坐标(称为U, V坐标)定义了纹理图形内部的一个点。U, V坐标对应于纹理图形的X, Y坐标,但是你并不是根据纹理图形以像素计量的宽度和高度来设定这些坐标,而是将这些坐标值设为0.0到1.0之间的范围。
一般来说,x-和y-坐标从0分别变动到图像的宽度和高度,因此如果你有一个640×480大小的图形,那么X会从0变动到639,而Y会从0变到479。为了访问一个图形中间的像素,你设X=319以及Y=239。
u-和v-坐标从0(图像的顶部边缘或左边缘)变到1(图像的右边缘或底部边缘),如图2.15所示。为了访问一个640×480的图像的中心像素,你使用坐标X=0.5以及Y=0.5。
u-和v-坐标的安排初看上去也许有点奇怪。然而,它用起来很棒,因为你可以迅速地swap out (不知道咋翻译……)不同尺寸的纹理,并且不需要担心其尺寸大小。
建议
===============================================================================
这里有一个非常棒的小技巧。你可以将u-或v-坐标的值设为大于1.0的数。这会导致纹理在被渲染到多边形的时候wrap around(不知道怎么翻译)。例如,如果你使用一个等于2.0的U值,那么纹理会在水平方向上被绘制两次(wrapped around once)。一个等于3.0的U值表示纹理被wrapped around三次。对于v-坐标来说同理。
===============================================================================
纹理可以是你想要的任何东西,虽然它们几乎总是位图格式的图像。图形硬件的最近的发展增加了凹凸映射技术(bump-mapping technology),它将一个纹理读入并将其看作一个粗糙的表面,使得被渲染的3-D物体看上去好像是坑坑洼洼的。
使用凹凸映射对于我们目前的目的来说有一点太高级了,所以为了保持事情简单,我只打算告诉你如何将纹理图形应用到一个多边形表面上,以改善你的图形系统的视觉外观。
纹理是由Direct3D通过一个IDirect3DTexture9对象而进行控制的。这个对象包含了纹理信息并提供了访问纹理信息的访问(包括一个指向纹理图像的像素信息的指针)。
在你第一次使用纹理的时候,你会开始注意到Direct3D和各个硬件制作商对它们的限制。首先,一个纹理的维数有限制,它应该是2的次幂(例如8,32,128或256)。一般来说你对纹理的宽度和高度要用同样的尺寸,例如128×128或256×256。然而要当心,因为在使用3-D图形的时候有一个陷阱:有一些图形卡不允许纹理的宽度和高度不一样(例如128×64或32×256),并且大多数(as of their writing)不允许非2的次幂的纹理尺寸。出于这些理由,你应该始终尽量使用宽高相等的纹理。此外,你应该确保你的纹理尺寸不会超过256×256,因为这是大多数图形卡能够操纵的最大的尺寸了(并且你得确保你的游戏是尽可能兼容的)。(不过这些限制到了现在基本上已经不存在了:现在使用512×512尺寸的纹理早就不是问题了。)
最后,不要使用太多的纹理。虽然渲染一个被纹理映射的多边形的过程对于图形硬件来说很容易处理,但是准备好使用纹理却并不简单。每当硬件需要一个纹理的时候,Direct3D和你的图形卡必须做一些工作来让自己做好跟纹理相关的准备。
这些工作包括将纹理复制到适当的内存中(如果它还没有在那里的话)以及设定颜色格式来匹配显示模式(还有其内部的颜色模式用法)。这个过程是对于系统和图形卡来说是耗时的,并且你用得越少就越好。
注意:
===============================================================================
市面上的大部分图形卡支持某种形式的直接内存(direct-memory)或者快速转换数据访问(fast-transfer data access),它们可以提升将纹理载入视频内存(video memory)的速度。我们可以安全地假设大多数图形卡是AGP接口类型的,这种类型支持超快类型转换(super-fast data transfer),这意味着你的纹理和其他的图形数据会尽可能快地被载入到你的图形卡的内存中。
===============================================================================
建议
===============================================================================
为了在图形卡准备纹理时减轻创建的负担,你可以将多个图片打包到单个的一个纹理图形中。这样做保证了纹理只需要建立一次;然后所有的图形在需要的时候就可以从纹理中拽出来。你会在本书剩下的部分中看到使用这一技术的例子的。
===============================================================================
为何得到纹理图形,你一般从磁盘或者其他的资源中将它们载入。事实上,D3DX库包含了好几个用来为你载入以及管理纹理的函数,让你的工作更加轻松。这些D3DX纹理载入函数展示在表2.5中。
表2.5 D3DX纹理载入函数
函数 |
描述 |
D3DXCreateTextureFromFile |
从一个位图文件载入一个纹理图形。 |
D3DXCreateTextureFromFileEx |
D3DXCreateTextureFromFile函数的一个高级版本。 |
D3DXCreateTextureFromFieInMemory |
从一个已经载入到内存中的文件载入一个纹理图形。 |
D3DXCreateTextureFromFieInMemoryEx |
D3DXCreateTextureFromFieInMemory函数的一个高级版本。 |
D3DXCreateTextureFromResource |
从资源中载入一个纹理图形。 |
D3DXCreateTextureFromResourceEx |
D3DXCreateTextureFromResource函数的一个高级版本。 |
你可以从表2.5中看到每个纹理载入函数都有两个版本,一个是载入纹理的快速、简单的版本,而那些以Ex结尾的函数是给予纹理创建过程更大控制的高级版本。为了开始你的纹理映射的长期冒险,首先来看看D3DXCreateTextureFromFile 函数,第一个也是最容易使用的函数:
HRESULTD3DXCreateTextureFromFile( IDirect3DDevice9 *pDevice, // pre-initialized device object LPCSTR pSrcFile, // filename of bitmap to load IDirect3DTexture9 **ppTexture); // texture object to create
同样地,这个函数不难处理;只需要将你创造的、之前初始化的3-D设备对象、你想要载入的位图图像的文件名以及指向你正在创建的IDirect3DTexture9对象的指针传递进去就行了。
这里是使用D3DXCreateTextureFromFile函数来载入一个叫做texture.bmp的位图到一个纹理对象中的一个例子:
// g_pD3DDevice =pre-initialized 3-D device object IDirect3DTexture9 *pD3DTexture; if(FAILED(D3DXCreateTextureFromFile(g_pD3DDevice, \ “texture.bmp”, (void**)&pD3DTexture))) { // Error occurred }
关于这个函数的一个很棒的事情就是它为你搞定了所有的初始化工作,并且将纹理“贴”到了D3DPOOL_MANAGED内存类(memory class)中,这意味着这个纹理会一直在内存之中(丢失纹理是前DX8程序员们必须要处理的一个很大的麻烦)。
正如前面“在Direct3D中使用纹理映射”那一节中提到的那样,一个3-D设备需要做好准备来使用纹理进行渲染。这个准备工作必须在一个多边形使用纹理进行渲染之前完成。如果你有1, 000个多边形,而每个多边形使用的都是不同的纹理,那么你就要跑遍每个多边形、设置其纹理、然后渲染之。
你重复这一过程,直到每个多边形都被渲染了。如果多个多边形使用同样的纹理,为了提高效率,那么设置纹理,然后渲染所有使用该纹理的多边形,而不要对每个多边形使用“设置、然后渲染”的循环。
为了设置纹理,使用IDirect3DDevice9::SetTexture函数:
HRESULTIDirect3DDevice9::SetTexture( DWORD Stage; // Texture stage 0-7 IDirect3DBaseTexture9 *pTexture); // Texture object to set
你明白将你创建的纹理对象传递到哪里(作为pTexture参数),但是那个Stage参数是啥呢?这叫做纹理层(texturestages),而这是Direct3D纹理映射技术最令人激动的技术之一。
用Direct3D进行纹理映射是非常多功能的。纹理并不一定要来自单独一个源,而是可以从多达8个不同的源中建立起来。这些源,称为纹理层,被标上了从0到7的数字。在渲染多边形的时候,对于每个要被绘制的像素,Direct3D开始于纹理层1(实际上应该是纹理层0)并查询一个纹理像素。从这继续,Direct3D移动到纹理层2(实际上应该是纹理层1)、寻求另一个纹理像素,或者允许你修改之前的纹理像素。这个过程会一直持续到所有的8个纹理层都被处理完毕。
每一个纹理层都可以随意改变纹理像素,包括将该像素与一个新的纹理像素进行混合(blend)、增加或减少颜色或亮度、甚至执行一种特殊的称为alpha混合(alpha blending)(一种将多个像素的颜色进行混合的技术)的效果。你可以在图2.16中看到这个过程,其中输入像素走过每个纹理层,从纹理层0开始——在这里一个像素的漫反射颜色成分(红、绿、蓝的等级)被从纹理中抽取出来。从这里继续,该像素被进行了alpha混合,然后被变暗,产生最终的要被渲染到显示屏上的输出像素。
有了纹理层后,就有了无限的可能性了,可是不幸的是,我没有足够的篇幅来进入这里的细节了。在这本书中,我只使用一个纹理层,它用来从纹理中提取像素颜色、运用多边形的颜色信息、并将最终得到的上过色的像素渲染到多边形上面。
下面的这块代码在纹理层0中选取了一个纹理以供使用,并告诉渲染器来抓取一个纹理像素、将顶点的颜色信息运用到上面、并关闭alpha混合:
// g_pD3DDevice is apre-initialized 3-D Device object // pD3DTexture is aloaded texture object // Set texture instage 0 g_pD3DDevice->SetTexture(0,pTexture); // Set stageparameters – only need to do this once in program g_pD3DDevice->SetTextureStageState(0, \ D3DTSS_COLOROP,D3DTOP_MODULATE); g_pD3DDevice->SetTextureStageState(0, \ D3DTSS_COLORARG1,D3DTA_TEXTURE); g_pD3DDevice->SetTextureStageState(0, \ D3DTSS_ALPHAOP,D3DTOP_DISABLE);
这是基本的纹理操作,因此你有可能经常看见它们。注意到你只要对stage state参数设置一次,而以后就只要依赖SetTexture函数的调用了。关于使用stage state参数的更多的信息,请参见DX SDK文档。
当你完成了纹理的使用后(在渲染完毕多边形之后),你再次调用SetTexture函数,但是将pTexture参数设为NULL,如下所示:
g_pD3DDevice->SetTexture(0,NULL);
这会将纹理从内存以及硬件处理器(hardware processor)中释放。如果不这样做的话,那么可能引起内存泄漏,甚至导致你的游戏崩溃。(实际上,有另外的释放纹理的方法;参见我的更新版代码或者“龙书”第二版。)
时不时地,你会看到人们提到采样器状态(sampler states)。采样器状态在使用纹理渲染多边形时起作用。因为显示屏的分辨率有限,所以图像会呈现出一些视觉上的不规则,例如在绘制对角线时,或者在纹理图形被放大时绘制其pixilated (oversized) samples (不知道怎么翻译) 时会出现锯齿状边缘。
处于这些原因(也许还有很多其他的原因),人们发明了取样器来消除这些小小的不完美。Direct3D使用很多取样器,它们能够天衣无缝地保证你的图形具有更加清晰的外观。
为了让Direct3D来使用一个取样器,你必须使用IDirect3DDevice9::SetSamplerState函数:
HRESULT IDirect3DDevice9::SetTextureStageState( DWORD Sampler, // Sampler/texture state 0-7 D3DSAMPLERSTATETYPE Type, // State to set DWORD Value); // Value to use
我在这本书中要使用的仅有的两个取样器是D3DSAMP_MINFILTER和D3DSAMP_MAXFILTER。这两个状态决定Direct3D如何在将像素输出到显示屏之前混合纹理内部的周围的像素。你在放大一个纹理的时候将第一个状态设为D3DTSS_MAGFILTER,而在缩小一个纹理时将其设为D3DTSS_MINFILTER。
参数Value可以是表2.6中列出来的之一。
表2.6 Direct3D纹理层状态过滤值(Texture Stage State Filter Values)
值 |
说明 |
D3DTEXF_NONE |
不使用过滤器。 |
D3DTEXF_POINT |
最快的过滤方式。从纹理映射中使用单独的一个像素。 |
D3DTEXF_LINEAR |
双线性插值模式。这个模式从纹理映射中的4个像素组合起来以产生一个混合的输出像素。这是一种相当快的、能够产生不错的、光滑的像素的模式。 |
D3DTEXF_ANISOTROPIC |
各向异性过滤对屏幕和纹理映射的多边形的角度差进行补偿。效果不错,但是速度慢。 |
一般来说,你要使用D3DTEXF_POINT或者D3DTEXF_LINEAR过滤模式;它们很快,而线性模式还可以产生光滑的输出。为了使用其中的一种过滤模式,使用下面的代码:
// g_pD3DDevice =pre-initialized device object // Set magnificationfilter if(FAILED(g_pD3DDevice->SetSamplerState(0, \ D3DSAMP_MAGFILTER, D3DTEXF_POINT))) { // Error occurred } // Set minificationfilter if(FAILED(g_pD3DDevice->SetSamplerState(0, \ D3DTSS_MINIFILTER, D3DTEXF_POINT))) { // Error occurred }
在一个物体(一个或者一列多边形)可以用纹理进行绘制之前,你必须保证多边形的顶点包含了一对U, V坐标。一个仅仅包含了一组3-D坐标和纹理坐标的自定义顶点结构体如下所示:
typedef struct { D3DVECTOR3 Position; // vertex position vector float tu, tv; // Adding texturecoordinates here! } sVertex;
到了这一步,你必须构造你的灵活顶点格式宏来告诉Direct3D你正在使用的顶点成分,而在这种情况下这些成分是未变换的3-D坐标以及一对纹理坐标。
使用D3DFVF_XYZ和D3DFVF_TEX1值可以完成此事:
#define VERTEXFMT(D3DFVF_XYZ | D3DFVF_TEX1)
现在到了有趣的部分——将你的图形放到屏幕上。加上几行代码,你可以扩展一个简单的多边形绘制函数,让它包含你的纹理。假设你已经初始化了设备、用纹理信息定义了顶点缓存、并且设置了世界、视角以及投影矩阵,那么下面就是载入一个纹理并用于绘制一个多边形组成的三角形列的一个示例:
// g_pD3DDevice =pre-initialized device object // NumPolys = numberof primitive polygons to draw // g_pD3DVertexBuffer= pre-created vertex buffer w/polygon info IDirect3DTexture9*pD3DTexture; // Texture object // Load the texture D3DXCreateTextureFromFile(g_pD3DDevice,“texture.bmp”, \ (void**)&pD3DTexture); if(SUCCEEDED(g_pD3DDevice->BeginScene())){ // Set the texture g_pD3DDevice->SetTexture(0, pD3DTexture); // Set the stream source and vertex shader g_pD3DDevice->SetStreamSource(0, g_pD3DVertexBuffer,0, \ sizeof(sVertex)); g_pD3DDevice->SetFVF(VERTEXFMT); // Draw triangle list g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0,NumPolys); // End the scene g_pD3DDevice->EndScene(); // Free the texture resources g_pD3DDevice->SetTexture(0, NULL); }
===============================================================================
终于讲完了第五节啦!这里面有一些函数的运用或许大家看得不是很明白。其实我也不明白,大家可以参考“龙书”第二版的第11章的内容。
另外,原书附赠了两个程序的代码,一个叫做Draw2D,是关于使用带2D坐标的顶点格式的;另一个叫做Draw3D,是关于使用带3D坐标的顶点格式的。初看起来,二者的运行结果貌似没啥区别,因为并不能观察到3D效果;但是实际上,Draw3D中的这个图片能够旋转,这就已经用到了世界变换了,而世界变换是3D坐标所独有的。当然,到了后面,大家可以看到越来越多能够体现出3D效果的程序!
下面是原始代码的下载地址:
原始代码
然后下面是本人对于这两个程序的更新版本:
更新版的Draw2D
更新版的Draw3D
毕竟,时代不同了,所以需要做出一些改进嘛!改进主要体现在下面这些方面:
1、 增加了几个文件。显然将所有的代码挤在一个文件里面不是一个好主意。这里增加的代码参考了Frank D. Luna的“龙书”第二版的代码。
2、 采用IDirect3DVertexDeclaration9 对象、配合D3DVERTEXELEMENT9 数组进行顶点格式的声明。这个是用于可编程渲染管道方法的,也是本人基本上会一直使用的方法。
3、 采用可编程渲染管道方法对图形进行渲染。所以你会发现代码中多了很多此书中从来没讲过的东西。这方面,还是那句话,请参考“龙书”第二版的内容!
4、 原先的图太无趣了,所以我就用了两幅美女图片。下面是程序运行时的截图: