本系列文章由zhmxy555(毛星云)编写,转载请注明出处。
文章链接: http://blog.csdn.net/zhmxy555/article/details/8632184
作者:毛星云(浅墨) 邮箱: happylifemxy@163.com
上篇文章中我们讲解了深度缓存的方方面面,有不少朋友都评论或者发邮件跟浅墨说接下来讲一讲和深度缓存情同手足的模板缓存相关的技术,于是,这篇文章就诞生了。这篇文章可是费了浅墨不少脑细胞啊,写了周末整整一天,一万多字,从早上11点写到晚上12点- -。
这篇文章的主角模板技术整体来说比深度测试技术难理解,文中有不懂的地方大家可以多看几遍,最好是结合文章后面我们提供的配套示例程序的代码一起理解。好了,我们开始正题吧。
一、对模板技术中概念的理解
想要学习模板技术,有两个首先的概念需要理解,即模板缓存与模板测试。
1、模板缓存
首先我们了解什么是模板缓存。
模板缓存(stencil buffer)是一个用于专门用于制作特效的离屏(off-screen)缓存。模板缓存的分辨率与之前讲过的后台缓存和深度缓存的分辨率完全相同,模板缓存的像素也后台缓存、深度缓存中的像素一一对应。正所谓人如其名,模板缓存,模板也,它能让我们动态地、有针对性地决定是否将某个像素写到后台缓存中。
比如,我们稍后会讲到的实现镜面特效,我们只需在镜子所在的那个特定的平面区域(注意是一片区域,不是整个平面)中绘制出最终幻想里的游戏角色“雷霆”的镜像,而不在镜子之外做多余的绘制。这个时候,模板缓存就可以派上用场了。
其实,模板缓存可以理解为Direct3D中的一个专门来做特效的工具缓存而已。
2、模板测试
在运用模板技术来进行特效的绘制时,需要精确到每个像素。我们会根据每个像素的模板缓存的值,进行一些检查,最后得出这个像素是否需要绘制的结论,从而实现一些特殊的效果。而这个检查的过程,就是模板测试。
在Direct3D中,我们常常利用模板测试来实现一些特殊的效果。比如图形的合成、镜面特效、消融、淡入淡出、轮廓的显示、侧影和实时阴影等等特效。
二、模板测试精细讲解
解释完基本概念,下面我们就来看看模板测试到底如何使用。
首先说一点,缓冲区和缓存是一个概念,都是根据buffer这个单词译过来的,只是根据语境的选择,有时候我们写作“缓冲区”,有时候我们写作“缓存”而已。
1.创建模板缓冲区
首先需要注意,Direct3D在创建深度缓冲区的同时创建了模板缓冲区,而且将深度缓冲区的一部分作为模板缓冲区使用,就好像上帝(Direct3D)在造人时先创造了亚当(深度缓冲区),再从亚当的身上取一块肋骨,于是这就有了夏娃(模板缓冲区)。笑:D
既然他们是同时创建的。那么他们如何创建相关的讲解也就是八九不离十。那么根据我们上篇文章《【Visual C++】游戏开发笔记四十五 浅墨DirectX教程十三 深度测试和Z缓存专场》里讲到的,深度缓冲区和模板缓冲区都是在Direct3D初始化时顺手创建的,我们在之前讲解Direct3D初始化时,在《Direct3D初始化四步曲之三:填内容》中就有提到。
回忆之前的Direct3D初始化四步曲知识,四步曲之三,其实从头到尾其实就是在填充一个D3DPRESENT_PARAMETERS结构体,下面我们先贴出这个结构体的原型:
typedef struct D3DPRESENT_PARAMETERS { UINT BackBufferWidth; UINT BackBufferHeight; D3DFORMAT BackBufferFormat; UINT BackBufferCount; D3DMULTISAMPLE_TYPE MultiSampleType; DWORD MultiSampleQuality; D3DSWAPEFFECT SwapEffect; HWND hDeviceWindow; BOOL Windowed; BOOL EnableAutoDepthStencil; D3DFORMAT AutoDepthStencilFormat; DWORD Flags; UINT FullScreen_RefreshRateInHz; UINT PresentationInterval; } D3DPRESENT_PARAMETERS,*LPD3DPRESENT_PARAMETERS;
在上篇文章中我们说和深度测试相关的参数有两个,第十个参数EnableAutoDepthStencil和第十一个参数AutoDepthStencilFormat。而今天的模板测试,只有第十一个参数与其相关,那我们就再用模板测试的口吻把这个参数讲一遍。
◆第十一个参数,D3DFORMAT类型的AutoDepthStencilFormat,指定AutoDepthStencilFormat的深度缓冲区和模板缓冲区共同的像素格式。具体格式可以在结构体D3DFORMAT中进行选取。我们列举一些可以选取的值:
D3DFMT_D16 深度缓存用16位存储每个像素的深度值
D3DFMT_D24X8 深度缓存用24位存储每个像素的深度值
D3DFMT_D32深度缓存用32位存储每个像素的深度值
另外提一点,如果针对老掉牙的机器,在创建模板缓冲区之前,需要检查一下当前的是否支持我们稍后填进去的模板缓冲区格式。也就是在我们的“Direct3D初始化四步曲之二:取信息”中取出信息来看一下我们的设备是否支持模板缓冲区格式,用到的是CheckDeviceFormat函数。因为现在的显卡普遍都功能全面,对Direct3D支持很好,很多时候我们并不需要专门去做这一步。
2.清除模板缓冲区
上篇文章结尾部分我们提了一下,Direct3D渲染五步曲的第一步里面用到的那个Clear方法里面也有和深度测试相关的内容,下面我们专门来讲一下。
Clear方法我们在渲染五步曲一文里面讲过,这里我们故地重游一下,也讲出点新东西来。
使用模板测试渲染每一帧之前,都需要先清除上一帧保存在模板缓冲区中的模板值。而清除模板缓冲、颜色缓冲区以及深度缓冲区都是这个IDirect3DDevice9::Clear方法的工作。
我们先贴出这个函数的原型:
HRESULT Clear( [in] DWORD Count, [in] const D3DRECT *pRects, [in] DWORD Flags, [in] D3DCOLOR Color, [in] float Z, [in] DWORD Stencil );
首先我们附上在《【Visual C++】游戏开发笔记三十四 浅墨DirectX提高班之三 起承转合的艺术:Direct3D渲染五步曲》一文中我们对于这个函数原封不动的讲解:
◆ 第一个参数,DWORD类型的Count,指定了接下来的一个参数pRect指向的矩形数组中矩形的数量。我们可以这样说,Count和pRects是一对好基友-o-。如果pRects我们将其设为NULL的话,这参数必须设为0。而如果pRects为有效的矩形数组的指针的话,这个Count必须就为一个非零值了。
◆ 第二个参数,const D3DRECT类型的*pRects,指向一个D3DRECT结构体的数组指针,表明我们需要清空的目标矩形区域。
◆ 第三个参数,DWORD类型的Flags,指定我们需要清空的缓冲区。它为D3DCLEAR_STENCIL、D3DCLEAR_TARGET、D3DCLEAR_ZBUFFER的任意组合,分别表示模板缓冲区、颜色缓冲区、深度缓冲区,用“|”连接。
◆ 第四个参数,D3DCOLOR类型的Color,用于指定我们在清空颜色缓冲区之后每个像素对应的颜色值,这里的颜色用D3DCOLOR表示,后面我们会讲到,这里我们只需要知道一种D3DCOLOR_XRGB(R,G, B)就可以了,这里的R,G,B为我们设定的三原色的值,都在0到255之间取值,比如D3DCOLOR_XRGB(123,76, 228)。
◆ 第五个参数,float类型的Z,用于指定清空深度缓冲区后每个像素对应的深度值。
◆ 第六个参数,DWORD类型的Stencil,用于指定清空模板缓冲区之后模板缓冲区中每个像素对应的模板值。
今天的重点是第三个参数,DWORD类型的Flags,指定我们需要清空的缓冲区。它为D3DCLEAR_STENCIL、D3DCLEAR_TARGET、D3DCLEAR_ZBUFFER的任意组合,分别表示模板缓冲区、颜色缓冲区、深度缓冲区,用“|”连接。
也就是说,我们想在调用Clear方法的时候清空哪个缓冲区,就在这里写上,想要清空多个就写上多个,用“|”连接。
如果我们三种缓冲区都要清理,就这样写:
g_pd3dDevice->Clear(0,NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(60, 150, 150), 1.0f, 0);
学到如今,这个三个缓冲区基本都介绍到了,所以我们之后的渲染五步曲的第一步就是这三个标识D3DCLEAR_STENCIL、D3DCLEAR_TARGET、D3DCLEAR_ZBUFFER都填了。
3.模板测试相关参数介绍
我们知道,使用模板测试实现各种效果的关键是正确设置于模板测试相关的各渲染状态。
什么,渲染状态?好吧,SetRenderState()函数又一次闪亮登场。我们在第一次介绍函数的时候说它的第一个参数在一个庞大的枚举类型D3DRENDERSTATETYPE中取值,下面我们看看D3DRENDERSTATETYPE中与模板测试相关的函数有哪些:
typedef enum D3DRENDERSTATETYPE { …………………… D3DRS_STENCILENABLE = 52, D3DRS_STENCILFAIL = 53, D3DRS_STENCILZFAIL = 54, D3DRS_STENCILPASS = 55, D3DRS_STENCILFUNC = 56, D3DRS_STENCILREF = 57, D3DRS_STENCILMASK = 58, D3DRS_STENCILWRITEMASK = 59, …………………… D3DRS_TWOSIDEDSTENCILMODE = 185, D3DRS_CCW_STENCILFAIL = 186, D3DRS_CCW_STENCILZFAIL = 187, D3DRS_CCW_STENCILPASS = 188, D3DRS_CCW_STENCILFUNC = 189, …………………… } D3DRENDERSTATETYPE,*LPD3DRENDERSTATETYPE;
这估计是我们《Visual C++游戏开发笔记》专栏开设以来,发表的四十六篇教程以来,第一次贴出这样不完整的数据结构来吧。下面我们对这些与模板相关的渲染状态挨个进行讲解:
■ D3DRS_STENCILENABLE:这个渲染状态用于启用或者禁用模板处理功能。这个参数指定为TRUE表示启用模板处理;指定为FALSE,则就表示禁用模板处理。
■ D3DRS_STENCILFAIL:这个渲染状态表示模板测试失败时进行的模板操作。而进行的模板操作默认为D3DSTENCILCAPS_KEEP。
■ D3DRS_STENCILZFAIL:该渲染状态表示模板测试通过时,但是深度测试失败时进行的模板操作。默认的模板操作依旧是D3DSTENCILCAPS_KEEP。
■ D3DRS_STENCILPASS:这个渲染状态表示模板测试通过时进行的模板操作。进行的模板操作默认依旧是为D3DSTENCILCAPS_KEEP。
■ D3DRS_STENCILFUNC:这个渲染状态可以指定用于模板测试的比较函数。比较函数可以是D3DCMPFUNC枚举常量之一,该比较函数将通过模板掩码的模板参考值与模板缓冲区中当前像素的对应模板值比较,如果为TRUE,则通过模板测试。
■ D3DRS_STENCILREF:这个渲染状态用于设置模板参考值,默认为0.
■ D3DRS_STENCILMASK:这个渲染状态用于设置模板掩码,决定对模板参考值和模板缓冲区值的哪位进行比较,默认掩码为0xffffffff。
■ D3DRS_STENCILWRITEMASK:这个渲染状态用于指定写入到模板缓冲区中的数值的掩码,默认掩码也为0xffffffff。
■ D3DRS_TWOSIDEDSTENCILMODE:这个渲染状态用于激活或者禁用双面缓冲区。
■ D3DRS_CCW_STENCILFAIL:这个渲染状态用于设置在启用了双面模板缓冲区后,顶点按照逆时针顺序组成的多边形当模板测试失败时进行的模板操作。
■ D3DRS_CCW_STENCILZFAIL:这个渲染状态用于设置在启用了双面模板缓冲区后,顶点按照逆时针顺序组成的多边形当模板测试成功但深度测试失败时进行的模板操作。
■ D3DRS_CCW_STENCILPASS:这个渲染状态用于设置在启用了双面模板缓冲区后,顶点按照逆时针顺序组成的多边形当模板测试成功时进行的模板操作。
■ D3DRS_CCW_STENCILFUNC:这个渲染状态指定了模板测试的比较函数,在我们上篇文章里讲过的D3DCMPFUNC枚举类型中取值,让我再一次贴出这枚举体的定义代码:
typedef enum D3DCMPFUNC { D3DCMP_NEVER = 1, D3DCMP_LESS = 2, D3DCMP_EQUAL = 3, D3DCMP_LESSEQUAL = 4, D3DCMP_GREATER = 5, D3DCMP_NOTEQUAL = 6, D3DCMP_GREATEREQUAL = 7, D3DCMP_ALWAYS = 8, D3DCMP_FORCE_DWORD = 0x7fffffff } D3DCMPFUNC, *LPD3DCMPFUNC;
下面我们通过一个表格,对这些枚举类型中的成员进行讲解说明:
枚举类型值(比较函数)
精析
D3DCMP_NEVER
深度测试函数总是返回FALSE
D3DCMP_LESS
测试点深度值小于深度缓冲区中相应值时,返回TRUE,为默认值
D3DCMP_QUAL
测试点深度值等于深度缓冲区中相应值时,返回TRUE
D3DCMP_LESSEQUAL
测试点深度值大于等于深度缓冲区中相应值时,返回TRUE
D3DCMP_GREATER
测试点深度值大于深度缓冲区中相应值时,返回TRUE
D3DCMP_NOTEQUAL
测试点深度值不等于深度缓冲区中相应值时,返回TRUE
D3DCMP_GREATEREQUAL
测试点深度值大于等于深度缓冲区中相应值时,返回TRUE
D3DCMP_ALWAYS
深度测试函数总是返回TRUE
D3DCMP_FORCE_DWORD
这个枚举值一般不用,用于保证将D3DCMPFUNC枚举类型编译为32位
对于目标表面上的每一个像素,Direct3D首先将应用程序定义的模板参考值和模板掩码进行逐位与运算,然后将当前测试的像素在模板缓冲区中的数值与模板掩码进行逐位与运算,最后根据模板比较函数对得到的结果进行比较,如果模板测试成功,也就是测试结果为true,那么该像素就被写入后台缓存;如果模板测试失败的话,也就是测试结果为false,那么该像素就不会被写入后台缓存,也不会被写入深度缓存。
另外,上面我们讲到的渲染状态 D3DRS_STENCILFAIL、D3DRS_STENCILZFAIL、D3DRS_STENCILPASS定义了模板测试、深度测试失败或者通过时进行的模板操作,他们也是在一个枚举类型中取值,这个枚举类型是D3DSTENCILOP,这个枚举类型的定义如下:
typedef enum D3DSTENCILOP { D3DSTENCILOP_KEEP = 1, D3DSTENCILOP_ZERO = 2, D3DSTENCILOP_REPLACE = 3, D3DSTENCILOP_INCRSAT = 4, D3DSTENCILOP_DECRSAT = 5, D3DSTENCILOP_INVERT = 6, D3DSTENCILOP_INCR = 7, D3DSTENCILOP_DECR = 8, D3DSTENCILOP_FORCE_DWORD =0x7fffffff } D3DSTENCILOP, *LPD3DSTENCILOP;我们还是用一个表格来讲解:
枚举类型值(模板操作)
精析
D3DSTENCILOP_KEEP
是默认的选项,表示不更新模板缓冲区中的值
D3DSTENCILOP_ZERO
将模板缓冲区中的值设为0
D3DSTENCILOP_REPLACE
用模板参考值替换模板缓冲区中对应的值
D3DSTENCILOP_INCRSAT
增加模板缓冲区中的对应数值,如果大于最大值,则等于最大值
D3DSTENCILOP_DECRSAT
减小模板缓冲区中的对应数值,如果小于最小值,则等于最小值
D3DSTENCILOP_INVERT
倒置模板测试区中的对应值的数据位
D3DSTENCILOP_INCR
增加模板缓冲区中对应数值,如果大于最大值,则等于0
D3DSTENCILOP_DECR
减小模板缓冲区中对应数值,如果小于0,则等于最大值
D3DSTENCILOP_FORCE_DWORD
这个枚举值一般不用,用于保证将D3DCMPFUNC枚举类型编译为32位
呼,这些参数终于介绍完了,再多介绍几个的话,恐怕大家就要到欧洲来去医院探望浅墨了- -。
4.对模板测试的一些理解
模板测试使用模板参考值、模板掩码、模板比较函数和当前像素在模板缓冲区中的模板值作为参数,判断某个像素是否将被写入到后台缓冲区中。模板测试的表达式是这样的:
其中的ref表示模板参考值,mask表示模板掩码,value表示模板缓冲中的值,OP表示模板比较函数,而符号“&”则表示模板值或模板参考值与模板掩码进行按位的与计算。
在Direct3D进行模板测试前,我们需要对模板测试的模板参考值、模板掩码和模板比较函数进行下设置。需要注意的是,模板参考值的默认值为0。当然,我们也可以自己亲手设置,用的依然是那个号称万能的SetRenderState。第一个参数参数渲染状态我们设为D3DRS_STENCILREF,而第二个参数就填一个数值(最好是填16进制的),表示需要的模板参考值。
举个小实例,下面这段代码我们就把模板参考值设为了1:
g_pd3dDevice->SetRenderState(D3DRS_STENCILREF,0x1);
而模板掩码用于屏蔽模板参考值和当前测试像素的模板值的某些位,上面我提到过,其默认值为0xffffffff,表示不屏蔽任何位。而对应的0x000000就表示屏蔽任何位。D3DRS_STENCILMASK与D3DRS_STENCILWRITEMASK这两个渲染状态在SetRenderState函数中就是分别表示模板掩码值和写掩码值的。
再举个小实例,下面这两句SetRenderState就是在设置模板掩码值和写掩码值,用于屏蔽模板参考值和像素模板值的低十六位:
g_pd3dDevice->SetRenderState(D3DRS_STENCILMASK, 0xffff0000); g_pd3dDevice->SetRenderState(D3DRS_STENCILWRITEMASK,0xffff0000);
由于在实用过程中对不同的特效要在SetRenderState中取不同的渲染状态,所以模板缓存很难总结出一个几步曲来,这个倒是有点可惜。如果上面这些知识听得不是很懂,没关系,下面我们可以在实例代码中亲身体会一下。
说曹操曹操到,接着我们就来看看模板测试的一个非常重要的应用——镜面特效。
、
三、镜面特效的实现
镜面特效是模板测试技术的应用中最简单的一个。三维游戏中模拟的自然界,有很多物体表面就可以看做是一块镜面,能反射其他物体的镜像。比如最常见的,水中的倒影、光滑地表上的人物镜像等等。
浅墨印象较深的是Dota2中飘逸的英雄船长昆卡的技能洪流释放之后,在地上会留下一潭水,有小兵或者英雄路过的时候,这潭水就会倒影出在这些小兵或者英雄的镜像来,非常的逼真。对了,Dota2用的引擎是Valve公司为著名的第一人称射击游戏《半条命2》系列所开发的Source游戏引擎。Source引擎也被我称为次世代引擎、起源引擎,采用C++开发,跨Microsoft Windows、Mac OS X、Xbox、Xbox360、PlayStation 3等众多平台。贴一张Source引擎的logo吧:
好了,我们继续来讲。
想要在Direct3D程序中实现镜面特效,首先需要计算出物体先归于特定平面中的镜像,而这个过程可以通过镜面成像的数学原理来进行计算,然后通过模板技术将物体的镜像正确地绘制到所指定的平面(镜面)中。
先来看一下镜面成像的原理图:
上图中,假设空间中有任意一点q,那么它相对于平面所成的像就为q'。
而已知q点的坐标,求出q'的坐标,就实现了我们镜面成像的目的。
其实,我们只要通过数学知识,求出q点到q'点的镜像变换矩阵就可以了,这样知道q点,根据镜像变换矩阵,就可以求出q'来。
这个镜像变换矩阵的求法,微软早就为我们准备好了,那就是D3DX库中的D3DXMatrixReflect函数。我们在MSDN中查到D3DXMatrixReflect的声明如下:
D3DXMATRIX* D3DXMatrixReflect( _Inout_ D3DXMATRIX *pOut, _In_ const D3DXPLANE *pPlane );
■ 第一个参数,D3DXMATRIX类型的*pOut,从类型上来看我们就知道他是一个D3DXMATRIX类型的4 X 4的矩阵,我们调用这个D3DXMatrixReflect方法,其实就是在为这个矩阵赋值,通过Direct3D的内部计算,让这个矩阵成为我们在第二个参数中提供的那个平面的镜像变换矩阵。
■ 第二个参数,const D3DXPLANE类型的*pPlane,显然就是一个D3DXPLANE结构体类型的平面了。
D3DXPLANE结构体我们之前没有遇到过,我们下面来简单介绍一下。MSDN中对于它是这样定义的:
typedef struct D3DXPLANE { FLOAT a; FLOAT b; FLOAT c; FLOAT d; } D3DXPLANE, *LPD3DXPLANE;
其中的a,b,c,d四个参数显然就是三维平面方程ax+by+cz=d的四个系数了。
在Direct3D中计算某个物体相对于任意平面的镜像时,我们只要通过这个D3DXMatrixReflect计算一下该平面的镜像变换矩阵,然后把该物体的世界变换矩阵乘以镜像变换矩阵就可以了,得到的结果就是世界变换矩阵。接着我们再SetMatrix一下,接着写渲染的代码就可以了。
//这里假如物体的原始世界矩阵是matWorld D3DXMATRIXmatReflect; D3DXPLANEplane(0.0f, 1.0f, 1.0f, 0.0f); // 定义平面方程为y+z=0的平面 D3DXMatrixReflect(&matReflect,&plane);//计算y+z=0平面的镜像变换矩阵 matWorld=matWorld*matReflect; //镜像变换矩阵和原始世界矩阵相乘,得到镜像的世界矩阵 g_pd3dDevice->SetTransform(D3DTS_WORLD,& matReflect);//设置出镜像的世界矩阵 //接下来就写绘制镜像的代码就可以了
另外说明一点,在我们当前还在讲解的固定渲染流水线中,微软为我们把和数学与物理原理相关的内容都封装起来了,很多时候,我们只要知道这些为我们封装好的函数如何使用,什么情况下使用就好了,而不去深究具体的实现细节。浅墨认为这是很明智的选择,无形中大大降低了Direct3D的入门难度。这又说明了我们学习Direct3D,先学固定功能渲染流水线,再学可编程渲染流水线,是最明智,学起来最轻松的路线。
为了降低学习门槛,让文章更加贴近大众,通俗易懂,我们也就暂时不深入讲解镜面成像的数学原理了,因为浅墨知道至少有不少看到数学公式就头疼的读者一直在读浅墨写的文章。:D
四、通过实例程序讲解
好了。镜面成像原理讲完了,我们接下来要着重看一下镜面特效的使用方法。对应镜面特效倒是可以整出一个几步曲来介绍,下面的讲解为了更加清楚,我们结合了本篇文章的配套源代码一起介绍。因为我们在讲的是渲染特效,所以代码精髓想都不用想,八九不离十就在Direct3D_Render()函数中。
这个实例程序中我们还是借助D3DXCreateBox来快捷创建一个薄板作为镜子,然后从X文件中载入一个3D人物并绘制出来(我们本次用的是最终幻想中帅气的女主角雷霆),接着就顺理成章地以这个薄板最为镜子,在镜子中绘制出3D人物模型“雷霆”的镜像。先放一张截图吧:
好吧,我们开始讲解。
Ⅰ. 清空模板缓存
第一步,在清空模板缓存,并将模板缓存的值都设为0,用Clear方法完成。这一步我们在Direct3D_Render()函数中渲染五步曲的第一步清屏里面已经做了,代码就是这样:
//-------------------------------------------------------------------------------------- //【Direct3D渲染五步曲之一】:清屏操作 //-------------------------------------------------------------------------------------- g_pd3dDevice->Clear(0,NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL, D3DCOLOR_XRGB(100,150, 0), 1.0f, 0);
Ⅱ.进行常规物体的绘制
这一步也就是包含了渲染五步曲的第二步“开始绘制”,以及第三步“正式绘制”。这一步里面的代码基本上就上一节介绍深度缓存时绘制人物模型和墙面的代码,没有什么新鲜的内容:
//-------------------------------------------------------------------------------------- //【Direct3D渲染五步曲之二】:开始绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->BeginScene(); // 开始绘制 //-------------------------------------------------------------------------------------- //【Direct3D渲染五步曲之三】:正式绘制 //-------------------------------------------------------------------------------------- D3DXMATRIXmatHero,matWorld,matRotation; //定义一些矩阵 //绘制3D模型 D3DXMatrixTranslation(&matHero,-20.0f, 0.0f, -25.0f); matHero=matHero*g_matWorld; g_pd3dDevice->SetTransform(D3DTS_WORLD,&matHero);//设置模型的世界矩阵,为绘制做准备 //用一个for循环,进行模型的网格各个部分的绘制 for(DWORD i = 0; i < g_dwNumMtrls; i++) { g_pd3dDevice->SetMaterial(&g_pMaterials[i]); //设置此部分的材质 g_pd3dDevice->SetTexture(0,g_pTextures[i]);//设置此部分的纹理 g_pMesh->DrawSubset(i); //绘制此部分 } //绘制镜面 D3DXMatrixTranslation(&matWorld,0.0f,0.0f,0.0f);//给墙面的世界矩阵初始化 g_pd3dDevice->SetTransform(D3DTS_WORLD,&matWorld);//设置墙面的世界矩阵 g_pd3dDevice->SetMaterial(&g_MaterialsWall);//设置材质 g_pMeshWall->DrawSubset(0);//绘制墙面
Ⅲ.启用模板缓存,以及对相关的绘制状态进行设置
调用一系列的方法来启用模板缓存,并且对模板比较函数、模板掩码以及更新模板缓存的渲染状态进行设置。用了一箩筐的SetRenderState,这一步的代码如下:
//3. 启用模板缓存,以及对相关的绘制状态进行设置。 g_pd3dDevice->SetRenderState(D3DRS_STENCILENABLE, true); g_pd3dDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS); g_pd3dDevice->SetRenderState(D3DRS_STENCILREF, 0x1); g_pd3dDevice->SetRenderState(D3DRS_STENCILMASK, 0xffffffff); g_pd3dDevice->SetRenderState(D3DRS_STENCILWRITEMASK,0xffffffff); g_pd3dDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE);
我们在上面的这段代码中,我们将模板比较函数指定为模板测试一直成功(D3DCMP_ALWAYS),这就意味着接下来我们绘制的函数总是能通过模板测试。同时,我们指定更新模板缓存的更新方式为D3DSTENCILOP_REPLACE,也就说,如果模板测试成功时用模板参考值(我们这里指定的为0x01)代替模板缓存中的值。
Ⅳ.进行融合操作
这一步里面,我们关闭向深度缓存中写的操作,然后启用融合操作。我们将源融合因子和目标融合因子分别指定为D3DBLEND_ZERO和D3DBLEND_ONE防止对后台缓存进行更新。
融合操作我们目前还没讲到过,其实也就是用SetRenderState进行一些渲染状态的设置而已,后面的文章里我们会花篇幅讲解的,这里不太理解不要紧,对于镜面特效我们讲解的这几步而言,其实除了绘制图形的那两步对于不同的程序不同以外,其他的几步代码都是千篇一律的。
//4.进行融合操作,以及禁止向深度缓存和后台缓存写数据 g_pd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, false); g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE,true); g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO); g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
Ⅴ.确定出镜面区域
这一步我们主要就是绘制出镜面区域,也就是指定出待会儿需要作为镜子的区域。因为我之前将模板比较函数设置为了D3DCMP_ALWAYS,所以镜面像素无论如何都可以通过模板测试。而且,我们之前还把模板缓存的更新方式设置为D3DSTENCILOP_REPLACE,那么在模板缓存中包含镜面区域的模板值就会被替换为1,而其他的区域的模板值仍然为0.这一步的代码如下:
//5.绘制出作为镜面的区域 D3DXMatrixTranslation(&matWorld,0.0f, 0.0f, 0.0f); g_pd3dDevice->SetTransform(D3DTS_WORLD,&matWorld); g_pd3dDevice->SetMaterial(&g_MaterialsWall); g_pMeshWall->DrawSubset(0);
Ⅵ. 重新设置一系列渲染状态
确定好镜面区域后,下面就来重新设置一下之前被改过的渲染状态和融合状态,为后面马上将要进行的镜像绘制做准备。把深度缓存的写操作打开,设置比较函数为D3DCMP_EQUAL,设置模板缓存的更新方式为当模板测试通过时保留模板缓冲中原来的值(也就是含有镜面区域的模板值为1时,其他区域的模板值为0),进行一些融合计算,将镜像与镜面进行融合。而且我们要关闭背面消隐,也就是将消隐模式设为D3DCULL_CW,这样我们在镜子中看到的才会是真实的物体背对着我们的那一面在镜子中的镜像,不然我们会看到非常奇葩不符合科学和生活常理的镜像出现。
另外,注意这个时候清空一下Z缓存,因为接下来我们所绘制的镜像的深度值必定会大于镜面的深度值,顺着镜面看的话,按常理镜像肯定是要被镜子遮挡住的,这样我们绘制镜像之前做的那么多工作就完全毁于一旦了。所以这个时候必定要调用Clear方法清理一下Z缓存。相关代码如下:
//6.重新设置一系列渲染状态,将镜像与镜面进行融合运算,并清理一下Z缓存 g_pd3dDevice->Clear(0,0, D3DCLEAR_ZBUFFER, 0, 1.0f, 0); g_pd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, true); g_pd3dDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL); g_pd3dDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP); g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR); g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO); g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
Ⅶ.计算镜像变换矩阵
这一步就是运用了我们在上面讲镜面特效时的思路,定义出镜面所在的平面的D3DXPLANE型平面,然后借助D3DXMatrixReflect来得到镜像变换矩阵。
//7.计算镜像变换矩阵 D3DXMATRIXmatReflect; D3DXPLANEplaneXY(0.0f, 0.0f, 1.0f, 0.0f); // xy平面 D3DXMatrixReflect(&matReflect,&planeXY); matWorld= matReflect * matHero;
Ⅷ.绘制镜像
忙了前面七步,就是为了现在不会吹灰之力地绘制出镜像。这一步完全没有技术含量,先设置一下世界矩阵,然后把第二步里面绘制物体的代码原封不动拷过来就行了:
//绘制镜子中的3D模型 g_pd3dDevice->SetTransform(D3DTS_WORLD,&matWorld);//设置模型的世界矩阵,为绘制做准备 //用一个for循环,进行模型的网格各个部分的绘制 for(DWORD i = 0; i < g_dwNumMtrls; i++) { g_pd3dDevice->SetMaterial(&g_pMaterials[i]); //设置此部分的材质 g_pd3dDevice->SetTexture(0,g_pTextures[i]);//设置此部分的纹理 g_pMesh->DrawSubset(i); //绘制此部分 }
Ⅸ.恢复渲染状态
因为我们的Direct3D_Render()函数在消息循环的驱动下一直在被调用,在绘制完镜像后,需要把渲染状态调回来,免得后面其他物体或者下一次调用Direct3D_Render()函数时的渲染受到影响。也就是关闭融合,关闭模板测试,打开背面消隐:
// 9.恢复渲染状态 g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false); g_pd3dDevice->SetRenderState( D3DRS_STENCILENABLE, false); g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
五、详细注释的源代码欣赏
本篇文章的配套源代码依旧是包含四个文件,主要用于公共辅助宏定义的D3DUtil.h,用于封装了DirectInput输入控制API的DirectInputClass.h和DirectInputClass.cpp最后才是核心代码main.cpp。
其实D3DUtil.h,DirectInputClass.h以及DirectInputClass.cpp在上篇文章的配套demo的基础上并没有做任何修改,我们只是修改了main.cpp中的代码而已。鉴于这个三个文件在之前的基础上无任何修改且在前面的文章中已经贴出过多次,这次就不再费篇幅贴出了,我们只贴出核心代码main.cpp即可:
//**************************************************