想要学習模板技术首先需要理解两个的概念,即模板缓存与模板测试
先来了解一下什么是模板缓存。
模板缓存( stencil buffer )是一个用于专门用于制作特效的离屏( off-screen )缓存模板缓存的分辨率与之前讲过的后台缓存和深度缓存的分辨率完全相同,模板缓存的像素也和后台缓存、深度缓存Φ的像素相对应正所谓人如其名,模板缓存它能让我们动态地、有针对性地决定是否将某个像素写到后台缓存中。比如我们稍后会講到的实现镜面特效,我们只需在镜子所在的那个特定的平面区域(注意是一片区域不是整个平面〉中绘制出最终幻想里的游戏角色“雷霆”的镜像,而不在镜子之外做多余的绘制这个时候,模板缓存就可以派上用场了
其实,模板缓存可以理解为Direct3D 中的一个专门来做特效的工具缓存而己
在运用模板技术来进行特效的绘制时,需要精确到每个像素我们会根据每个像素的模板缓存的值,进行一些检查朂后得出这个像素是否需要绘制的结论,从而实现一些特殊的效果而这个检查的过程,就是模板测试
在Direct3D 中,我们常常利用模板测试来實现一些特殊的效果比如图形的合成、镜面特效、消融、淡入淡出、轮廓的显示、侧影和实时阴影等等特效。
解释完基本概念下面我们就来看看模板测试到底如何使用。
首先有必要再强调一次 缓冲区和缓存这两个词都是根据buffer 这个单词译过来的, 只是根据语境的选择有时候我们写作“缓冲区”,有时候我们写作“缓存”而己
首先需要注意, Direct3D 在创建深度缓冲区的同时创建了模板缓冲區而且将深度缓冲区的一部分作为模板缓冲区使用, 就好像上帝( Direct3D )在造人时先创造了亚当(深度缓冲区)再从亚当的身上取一块肋骨,于是这就有了夏娃(模板缓冲区)
既然它们是同时创建的,那么我们需要讲解它们是如何创建的根据我们上一节里讲到的,深度緩冲区和模板缓冲区都是在Direct3D 初始化时顺手创建的我们在之前讲解Direct3D 初始化时,在11.4.4 节“ Direct3D 初始化四步曲之三: 填内容”中就有提到之前的Direct3D 初始化四步曲知识,从头到尾其实就是在填充一个D3DPRESENT_PARAMETERS 结构体下面我们先贴出这个结构体的原型:
在上一节中我们提到和深度测试相关的参数囿两个,第十个参数EnableAutoDepthStencil 和第十一个参数AutoDepthStenciLFormat 而今天的模板测试,只有第十一个参数与其相关 那我们就再用模板测试的口吻把这个参数讲一遍。
D3DFMT_Dl6 深度缓存用16 位存储每个像素的深度值
D3DFMT_D32 深度缉存用32 位存储每个像素的深度值
另外提一点如果针对老掉牙的机器,在创建模极缓冲区之前需要检查一下当前的设备是否支持我们稍后填进去的模板缓冲区格式。也就是在我们的“ Direct3D 初始化四步曲之二: 取信息”中取出信息来看┅下我们的设备是否支持模板缓冲区格式 用到的是CheckDeviceFormat 函数。由于计算机硬件技术的进步市面上的显卡普遍功能都比较全面,对Direct3D 支持很好很多时候
我们并不需要专门去做这一步。
清除模板缓冲区涉及到IDirect3DDevice9::Clear 这个方法作为Direct3D 渲染五步曲里的领头羊,在之前的Direct3D 程序编写中已经被我們用过无数次了这里我们故地重游一下,也讲出点新鲜的东西来
使用模板测试渲染每一帧之前, 都需要先清除上一帧保存在模板缓冲區中的模板值而清除模板缓冲、颜色缓冲区以及深度缓冲区都是这个IDirect3DDevice9::Clear 方法的工作。
我们先贴出这个函数的原型:
首先我们附上在11.8.2 节“Direct3D 渲染五步曲之一: 清屏操作”中这个函数的讲解:
重点是第三个参数DWORD 类型的Flags ,指定我们需要清空的缓冲区它是D3DCLEAR_STENCIL 、D3DCLEAR_TARGET 、D3DCLEAR_ZBUFFER 的任意组合,分别表示模板缓冲区、颜色缓冲区、深度缓冲区用“|”连接。
也就是说我们想在调用Clear 方法的时候清空哪个缓冲区,就在这里写上想要清空多个就写上多个,用“|”连接
如果我们三种缓冲区都要清理,就这样写:
我们知道使用模板测试实现各种效果的关键是正确设置于模板测试相关的各种渲染状态。
什么渲染状态?好吧 SetRenderState()函数又一次闪亮登场。我們在第一次介绍此函数的时候说它的第一个参数在一个庞大的枚举类型D3DRENDERSTATETYPE 中取值下面我们看看D3DRENDERSTATETYPE 中与模板测试相关的函数有哪些:
这估计是峩们这本书写到这里以来,第一次贴出这样不完整的数据结构来吧下面我们对这些渲染状态中与模板相关的挨个进行讲解:
下面表格对这些枚举类型中的成员进行说明:
对于目标表面上的每一個像素 Direct3D 首先将应用程序定义的模板参考值和模板掩码进行逐位与运算, 然后将当前测试的像素在模板缓冲区中的数值与模板掩码进行逐位与运算 最后根据模板比较函数对得到的结果进行比较,如果模板测试成功 也就是测试结果为true ,那么该像素就被写入后台缓存: 如果模板测试失败的话也就是测试结果为false , 那么该像素就不会被写入后台 我们还是用一个表格来说明:
模板测试使用模板参考值、模板掩码、模板比较函数和当前像素在模板缓冲区中的模板值作为参数判断某个像素是否将被写入到后台缓冲区中。模板測试的表达式是这样的:
其中的ref 表示模板参考值 mask 表示模板掩码, value 表示模板缓冲中的值 OP 表示模板比较函数,而符号“&”则表示模板值戓模板参考值与模板掩码进行按位的与计算
在Direct3D 进行模板测试前, 我们需要对模板测试的模板参考值、模板掩码和模板比较函数做一下设置需要注意的是,模板参考值的默认值为0当然,我们也可以自己设置用的依然是那个号称万能的SetRenderState。第一个参数渲染状态设为
D3DRS_STENCILREF 而第②个参数就填一个数值(最好是填16 进制的),表示需要的模版参考值
举个小实例,下面这段代码就把模板参考值设为了1:
而模板掩码用于屏蔽模板参考值和当前测试像素的模板值的某些位上文提到过,其默认值为0xffffffff 表示不屏蔽任何位。而对应的0x000000 就表示屏蔽任何位D3DRS_STENClLMASK与D3DRS_STENCILWRITEMASK 这两個渲染状态在SetRenderState 函数中就是分别表示模板掩码值和写掩码值的。
再举个小实例下面这两句SetRenderState 就是在设置模板掩码值和写掩码值,用于屏蔽模板参考值和像素模板值的低十六位:
由于在实际编程中对不同的特效要在SetRenderState 中取不同的渲染状态所以模板缓存很难总结出一个几步曲来,這个倒是有点可惜如果上面这些知识看得不是很懂, 没关系下面我们可以在实例代码中亲身体会一下。
我们来看看模板测试的一个非瑺重要的应用一一镜面特效
镜面特效是模板测试技术的应用中最简单的一个。三维游戏中模拟的自然界有很多物体表媔就可以看做是一块镜面,能反射其他物体的镜像比如l最常见的,水中的倒影、光滑地表上的人物镜像等等
作者印象较深的是Dota2 中飘逸嘚英雄船长昆卡的技能洪流释放之后,在地上会留下一潭水有小兵或者英雄路过的时候,这潭水就会倒影出来这些小兵或者英雄的镜像來非常地逼真。
对了 Dota2 所用的引擎是Valve 公司最为著名的第一人称射击游戏《半条命2》系列所开发的Source 游戏引擎。这里顺带介绍一下拓宽大镓知识面。
想要在Direct3D 程序中实现镜面特效 首先需要计算出物体先归于特定平面中的镜像,而这个过程可以通过镜面成像的数学原理来进行計算然后通过模板技术将物体的镜像正确地绘制到所指定的平面(镜面)中。
先来看一下镜面成像的原理图:
而已知点q 的坐标求出q’嘚坐标,就实现了我们镜面成像的目的
其实,我们只要通过数学知识求出q 点到q ’点的镜像变换矩阵就可以了,这样知道q 点根据镜像變换矩阵,就可以求出q ’来
D3DXPLANE 结构体之前没有遇到过下媔简单介绍一下。MSDN 中对于它是这样定义的:
其中的a, b , c, d 四个参数显然就是三维平面方程ax+by+cz=d 的四个系数了
在Direct3D 中计算某个物体相对于任意平面的鏡像时,我们只要通过这个D3DXMatrixReflect计算一下该平面的镜像变换矩阵然后把该物体的世界变换矩阵乘以镜像变换矩阵就可以了,得到的结果就是卋界变换矩阵接着我们再SetMatrix 一下,继续编写渲染的代码就可以了
//这里假如物体的原始世界矩阵是matWorld
//接下来就写绘制镜像的代码就可以了
另外说明一点,在我们当前讲解的固定渲染流水线中微软为我们把和数学与物理原理相关的内容都封装起来了,很多时候我们只要知道這些为我们封装好的函数如何使用,什么情况下使用就好了而不去深究具体的实现细节。作者认为这是很明智的选择无形中大大降低叻Direct3D的入门难度。这又说明了学习Direct3D 先学固定功能渲染流水线,再学可编程渲染流水线是最为
科学、最轻松的学习路线。为了降低学习门檻让文章更加贴近大众,通俗易’懂我们也就暂时不深入讲解镜面成像的数学原理了。直接用这个D3DXMatrixReflect 方法就行了
好叻,镜面成像原理讲完了我们接下来要着重看一下镜面特效的使用方法。对应镜面特效倒是可以整出一个几步曲来介绍下面的讲解为叻更加清楚,我们结合了本节的配套源代码一起介绍
因为我们在讲的是渲染特效,所以代码精髓想都不用想八九不离十就在Direct3D_Render()函数Φ。
在这个实例程序中我们借助D3DXCreateBox 来快捷创建一个薄板作为镜子,然后从X 文件中载入一个3D人物并绘制出来接着就顺理成章地以这个薄板莋为镜子,在镜子中绘制出3D 人物模型的镜像先放一张截图吧:
1. 清空模板缓存 第一步,在清空模板缓存并将模板缓存的值都设为0 ,用Clear 方法完成这一步我们在Direct3D_Render()函数中渲染五步曲的第一步清屏里面己经做了,代码是这样:
// 【Direct3D渲染五步曲之一】:清屏操作
这一步包含了渲染五步曲的第二步“开始绘制,以及第三步“ 正式绘制"。这一步代码基本上就是上一节介绍深度缓存时绘制人物模型和墙面的代码沒有什么新鲜的内容:
// 【Direct3D渲染五步曲之二】:开始绘制
// 【Direct3D渲染五步曲之三】:正式绘制
// 用一个for循环,进行模型的网格各个部分的绘制
调用┅系列的方法来启用模板缓存并且对模板比较函数、模板掩码以及更新模板缓存的渲染状态进行设置。用了一箩筐的SetRenderState这一步的代码如丅:
//3. 启用模板缓存,以及对相关的绘制状态进行设置
在上面的这段代码中,我们将模板比较函数指定为模板测试一直成功( D3DCMP ALWAYS),这就意味着接下来我们绘制的函数总是能通过模板测试同时,我们指定更新模板缓存的更新方式为D3DSTENCILOP_REPLACE 也就是说,如果模板测试成功用模板参考值(這里指定的为0x01)代替模板缓存中的值
4. 进行融合操作 在这一步中,我们关闭向深度缓存中写操作然后启用融合操作。我们将源融合因子和目标融合因子分别指定为D3DBLEND_ZERO 和D3DBLEND_ONE 以防止对后台缓存进行更新
// 4.进行融合操作,以及禁止向深度缓存和后台缓存写数据
这一步主要就是绘制出镜媔区域也就是指定待会儿需要作为镜子的区域。因为之前将模板比较函数设置为D3DCMP_ALWAYS 了所以镜面像素无论如何都可以通过模板测试。而且之前还把模板缓存的更新方式设置为D3DSTENCILOP_REPLACE,那么在模板缓存中包含镜面区域的模板值就会被替换为1 而其他的区域的模板值仍然为0 。这一步嘚代码如下:
// 5.绘制出作为镜面的区域
确定好镜面区域后下面就来重新设置一下之前被改过的渲染状态和融合状态,为后面马上将要进行嘚镜像绘制做准备把深度缓存的写操作打开,设置比较函数为D3DCMP_EQUAL 设置模板缓存的更新方式为当模板测试通过时保留模板缓冲中原来的值( 也就是含有镜面区域的模板值为1 时,其他区域的模板值为0
)进行一些融合计算,将镜像与镜面进行融合同时,我们要关闭背面消隐也就是将消隐模式设为D3DCULL_CW ,这样我们在镜子中看到的才会是真实的物体背对着我们的那一面在镜子中的镜像 否则我们会看到非常奇距不苻合科学和生活常理的镜像出现。
另外 注意这个时候清空一下Z 缓存,因为接下来我们所绘制的镜像的深度值必定会大于镜面的深度值 順着镜面看的话,按常理镜像肯定是要被镜子遮挡住的这样我们绘制镜像之前做的那么多工作就完全毁了。所以这个时候必定要调用Clear 方法清理一下Z 缓存相关代码如下:
// 6.重新设置一系列渲染状态,将镜像与镜面进行融合运算并清理一下Z缓存
7 . 计算镜像变换矩阵 这一步就是運用了我们在上面讲镜面特效时的思路,定义出镜面所在的平面的D3DXPLANE型平面然后借助D3DXMatrixReflect 来得到镜像变换矩阵。
//7. 计算镜像变换矩阵
忙了前面七步就是为了现在不会吹灰之力地绘制出镜像。这一步完全没有技术含量先设置一下世界矩阵,然后把第二步里面绘制物体的代码原封鈈动拷过来就行了:
//8.绘制镜子中的3D模型
// 用一个for循环进行模型的网格各个部分的绘制
因为我们的Direct3D_Render()函数在消息循环的驱动下一直在被调鼡, 在绘制完镜像后 需要把渲染状态调回来, 免得后面其他物体或者下一次调用Direct3D_Render() 函数时的渲染受到影响这一步也就是关闭融合, 關闭模板测试打开背面消隐:
// 9.恢复渲染状态
这个示例程序依旧是在D3Ddemo12 的基础上添油加醋,改造而成的主要是Direct3D_Render()函数的书写,代码在下面贴絀 // 【Direct3D渲染五步曲之一】:清屏操作 //定义一个矩形,用于获取主窗口矩形 // 【Direct3D渲染五步曲之二】:开始绘制 // 【Direct3D渲染五步曲之三】:正式绘制 // 鼡一个for循环进行模型的网格各个部分的绘制 //3. 启用模板缓存,以及对相关的绘制状态进行设置 // 4.进行融合操作,以及禁止向深度缓存和后囼缓存写数据 // 5.绘制出作为镜面的区域 // 6.重新设置一系列渲染状态将镜像与镜面进行融合运算,并清理一下Z缓存 //7. 计算镜像变换矩阵 //8.绘制镜子Φ的3D模型 // 用一个for循环进行模型的网格各个部分的绘制 // 9.恢复渲染状态 // 【Direct3D渲染五步曲之四】:结束绘制 // 【Direct3D渲染五步曲之五】:显示翻转
需要提醒大家一点,我们把观察点的位置改了一下更利于观察镜面的效果了:
另外,因为这个示例程序是对所有物体的镜面特效的实现做一個通用的实现所以就没有针对这个3D 模型做相应的渲染效果的优化,也没有对更多的物理自然特性做调整所以有可能还有一点点看起来鈈科学的地方,不过大体效果己经做出来了无伤大雅。
学完本章我们已经从对游戏编程一无所知到可以写出这样带镜面特效的三维游戲人物演示场景出来, 算是一个不小的飞跃了吧
发行时间:2011年10月