1、模板缓存

 

首先我们了解什么是模板缓存。

模板缓存(stencil buffer)是一个用于专门用于制作特效的离屏(off-screen)缓存。模板缓存的分辨率与之前讲过的后台缓存和深度缓存的分辨率完全相同,模板缓存的像素也后台缓存、深度缓存中的像素一一对应。正所谓人如其名,模板缓存,模板也,它能让我们动态地、有针对性地决定是否将某个像素写到后台缓存中。

比如,我们稍后会讲到的实现镜面特效,我们只需在镜子所在的那个特定的平面区域(注意是一片区域,不是整个平面)中绘制出最终幻想里的游戏角色“雷霆”的镜像,而不在镜子之外做多余的绘制。这个时候,模板缓存就可以派上用场了。

其实,模板缓存可以理解为Direct3D中的一个专门来做特效的工具缓存而已。

 

2、模板测试

 

在运用模板技术来进行特效的绘制时,需要精确到每个像素。我们会根据每个像素的模板缓存的值,进行一些检查,最后得出这个像素是否需要绘制的结论,从而实现一些特殊的效果。而这个检查的过程,就是模板测试。

在Direct3D中,我们常常利用模板测试来实现一些特殊的效果。比如图形的合成、镜面特效、消融、淡入淡出、轮廓的显示、侧影和实时阴影等等特效。

 

1.创建模板缓冲区

 

首先需要注意,Direct3D在创建深度缓冲区的同时创建了模板缓冲区,而且将深度缓冲区的一部分作为模板缓冲区使用,就好像上帝(Direct3D)在造人时先创造了亚当(深度缓冲区),再从亚当的身上取一块肋骨,于是这就有了夏娃(模板缓冲区)。笑:D

既然他们是同时创建的。那么他们如何创建相关的讲解也就是八九不离十。那么根据我们上篇文章《【Visual C++】游戏开发笔记四十五 浅墨DirectX教程十三 深度测试和Z缓存专场》里讲到的,深度缓冲区和模板缓冲区都是在Direct3D初始化时顺手创建的,我们在之前讲解Direct3D初始化时,在《Direct3D初始化四步曲之三:填内容》中就有提到。

回忆之前的Direct3D初始化四步曲知识,四步曲之三,其实从头到尾其实就是在填充一个D3DPRESENT_PARAMETERS结构体,下面我们先贴出这个结构体的原型:

 

[cpp] view plain copy
 
 print?
  1. typedef struct D3DPRESENT_PARAMETERS {  
  2.  UINT               BackBufferWidth;  
  3.  UINT               BackBufferHeight;  
  4.  D3DFORMAT          BackBufferFormat;  
  5.  UINT               BackBufferCount;  
  6.  D3DMULTISAMPLE_TYPE MultiSampleType;  
  7.  DWORD               MultiSampleQuality;  
  8.  D3DSWAPEFFECT       SwapEffect;  
  9.  HWND                hDeviceWindow;  
  10.  BOOL                Windowed;  
  11.  BOOL               EnableAutoDepthStencil;  
  12.  D3DFORMAT          AutoDepthStencilFormat;  
  13.  DWORD               Flags;  
  14.  UINT                FullScreen_RefreshRateInHz;  
  15.  UINT               PresentationInterval;  
  16. } 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方法的工作。

我们先贴出这个函数的原型:

 

[cpp] view plain copy
 
 print?
  1. HRESULT Clear(  
  2.  [in]  DWORD Count,  
  3.  [in]  const D3DRECT *pRects,  
  4.  [in]  DWORD Flags,  
  5.  [in]  D3DCOLOR Color,  
  6.  [in]  float Z,  
  7.   [in]  DWORD Stencil  
  8. );  

 

 

首先我们附上在《【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方法的时候清空哪个缓冲区,就在这里写上,想要清空多个就写上多个,用“|”连接。

如果我们三种缓冲区都要清理,就这样写: 

      

[cpp] view plain copy
 
 print?
  1. 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中与模板测试相关的函数有哪些:

 

[cpp] view plain copy
 
 print?
  1. typedef enum D3DRENDERSTATETYPE {  
  2. ……………………  
  3. D3DRS_STENCILENABLE                = 52,  
  4.  D3DRS_STENCILFAIL                 = 53,  
  5.  D3DRS_STENCILZFAIL                = 54,  
  6.  D3DRS_STENCILPASS                 = 55,  
  7.  D3DRS_STENCILFUNC                 = 56,  
  8.  D3DRS_STENCILREF                  = 57,  
  9.  D3DRS_STENCILMASK                 = 58,  
  10.  D3DRS_STENCILWRITEMASK            = 59,  
  11. ……………………  
  12.    
  13. D3DRS_TWOSIDEDSTENCILMODE          = 185,  
  14.  D3DRS_CCW_STENCILFAIL             = 186,  
  15.  D3DRS_CCW_STENCILZFAIL            = 187,  
  16.  D3DRS_CCW_STENCILPASS             = 188,  
  17.  D3DRS_CCW_STENCILFUNC             = 189,  
  18. ……………………  
  19.    
  20. } 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枚举类型中取值,让我再一次贴出这枚举体的定义代码:

 

[cpp] view plain copy
 
 print?
  1. typedef enum D3DCMPFUNC {  
  2.  D3DCMP_NEVER          = 1,  
  3.  D3DCMP_LESS           = 2,  
  4.  D3DCMP_EQUAL          = 3,  
  5.  D3DCMP_LESSEQUAL      = 4,  
  6.  D3DCMP_GREATER        = 5,  
  7.  D3DCMP_NOTEQUAL       = 6,  
  8.  D3DCMP_GREATEREQUAL   = 7,  
  9.  D3DCMP_ALWAYS         = 8,  
  10.  D3DCMP_FORCE_DWORD    = 0x7fffffff  
  11. } 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,这个枚举类型的定义如下:

 

[cpp] view plain copy
 
 print?
  1. typedef enum D3DSTENCILOP {  
  2.  D3DSTENCILOP_KEEP          = 1,  
  3.  D3DSTENCILOP_ZERO          = 2,  
  4.  D3DSTENCILOP_REPLACE       = 3,  
  5.  D3DSTENCILOP_INCRSAT       = 4,  
  6.  D3DSTENCILOP_DECRSAT       = 5,  
  7.  D3DSTENCILOP_INVERT        = 6,  
  8.  D3DSTENCILOP_INCR          = 7,  
  9.  D3DSTENCILOP_DECR          = 8,  
  10.  D3DSTENCILOP_FORCE_DWORD   =0x7fffffff  
  11. } 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:

 

[cpp] view plain copy
 
 print?
  1. g_pd3dDevice->SetRenderState(D3DRS_STENCILREF,0x1);  

 

 

而模板掩码用于屏蔽模板参考值和当前测试像素的模板值的某些位,上面我提到过,其默认值为0xffffffff,表示不屏蔽任何位。而对应的0x000000就表示屏蔽任何位。D3DRS_STENCILMASK与D3DRS_STENCILWRITEMASK这两个渲染状态在SetRenderState函数中就是分别表示模板掩码值和写掩码值的。

 

再举个小实例,下面这两句SetRenderState就是在设置模板掩码值和写掩码值,用于屏蔽模板参考值和像素模板值的低十六位:

 

[cpp] view plain copy
 
 print?
  1. g_pd3dDevice->SetRenderState(D3DRS_STENCILMASK,      0xffff0000);  
  2. 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的声明如下:

 

 

[cpp] view plain copy
 
 print?
  1. D3DXMATRIX* D3DXMatrixReflect(  
  2.   _Inout_  D3DXMATRIX *pOut,  
  3.   _In_     const D3DXPLANE *pPlane  
  4. );  

 

 

■ 第一个参数,D3DXMATRIX类型的*pOut,从类型上来看我们就知道他是一个D3DXMATRIX类型的4 X 4的矩阵,我们调用这个D3DXMatrixReflect方法,其实就是在为这个矩阵赋值,通过Direct3D的内部计算,让这个矩阵成为我们在第二个参数中提供的那个平面的镜像变换矩阵。

■ 第二个参数,const D3DXPLANE类型的*pPlane,显然就是一个D3DXPLANE结构体类型的平面了。

D3DXPLANE结构体我们之前没有遇到过,我们下面来简单介绍一下。MSDN中对于它是这样定义的:

 

[cpp] view plain copy
 
 print?
  1. typedef struct D3DXPLANE {  
  2.   FLOAT a;  
  3.   FLOAT b;  
  4.   FLOAT c;  
  5.   FLOAT d;  
  6. } D3DXPLANE, *LPD3DXPLANE;  

 

 

其中的a,b,c,d四个参数显然就是三维平面方程ax+by+cz=d的四个系数了。

在Direct3D中计算某个物体相对于任意平面的镜像时,我们只要通过这个D3DXMatrixReflect计算一下该平面的镜像变换矩阵,然后把该物体的世界变换矩阵乘以镜像变换矩阵就可以了,得到的结果就是世界变换矩阵。接着我们再SetMatrix一下,接着写渲染的代码就可以了。

 

[cpp] view plain copy
 
 print?
  1. //这里假如物体的原始世界矩阵是matWorld  
  2.       D3DXMATRIXmatReflect;  
  3.       D3DXPLANEplane(0.0f, 1.0f, 1.0f, 0.0f); // 定义平面方程为y+z=0的平面  
  4.       D3DXMatrixReflect(&matReflect,&plane);//计算y+z=0平面的镜像变换矩阵  
  5. matWorld=matWorld*matReflect;  //镜像变换矩阵和原始世界矩阵相乘,得到镜像的世界矩阵  
  6. g_pd3dDevice->SetTransform(D3DTS_WORLD,& matReflect);//设置出镜像的世界矩阵  
  7. //接下来就写绘制镜像的代码就可以了  

另外说明一点,在我们当前还在讲解的固定渲染流水线中,微软为我们把和数学与物理原理相关的内容都封装起来了,很多时候,我们只要知道这些为我们封装好的函数如何使用,什么情况下使用就好了,而不去深究具体的实现细节。浅墨认为这是很明智的选择,无形中大大降低了Direct3D的入门难度。这又说明了我们学习Direct3D,先学固定功能渲染流水线,再学可编程渲染流水线,是最明智,学起来最轻松的路线。