20.1 对模板技术中概念的理解
想要学习模板技术,首先需要理解两个的概念,即模板缓存与模板测试。20.1.1 模板缓存
先来了解一下什么是模板缓存。模板缓存( stencil buffer )是一个用于专门用于制作特效的离屏( off-screen )缓存。模板缓存 的分辨率与之前讲过的后台缓存和深度缓存的分辨率完全相同,模板缓存的像素也和后台缓存、深度 缓存中的像素相对应。正所谓人如其名,模板缓存,它能让我们动态地、有针对性地决定是否将 某个像素写到后台缓存中。
比如,我们稍后会讲到的实现镜面特效,我们只需在镜子所在的那个特定的平面区域(注意是 一片区域,不是整个平面〉中绘制出最终幻想里的游戏角色“雷霆”的镜像,而不在镜子之外做多 余的绘制。这个时候,模板缓存就可以派上用场了。
其实,模板缓存可以理解为Direct3D 中的一个专门来做特效的工具缓存而己。
20.1.2 模板测试
在运用模板技术来进行特效的绘制时,需要精确到每个像素。我们会根据每个像素的模板缓存的值,进行一些检查,最后得出这个像素是否需要绘制的结论,从而实现一些特殊的效果。而这个检查的过程,就是模板测试。在Direct3D 中,我们常常利用模板测试来实现一些特殊的效果。比如图形的合成、镜面特效、消融、淡入淡出、轮廓的显示、侧影和实时阴影等等特效。
20.2 模板测试精细讲解
解释完基本概念,下面我们就来看看模板测试到底如何使用。首先有必要再强调一次, 缓冲区和缓存这两个词都是根据buffer 这个单词译过来的, 只是根据 语境的选择,有时候我们写作“缓冲区”,有时候我们写作“缓存”而己。
20.2.1 创建模板缓冲区
首先需要注意, Direct3D 在创建深度缓冲区的同时创建了模板缓冲区,而且将深度缓冲区的一 部分作为模板缓冲区使用, 就好像上帝( Direct3D )在造人时先创造了亚当(深度缓冲区),再从 亚当的身上取一块肋骨,于是这就有了夏娃(模板缓冲区)。既然它们是同时创建的,那么我们需要讲解它们是如何创建的。根据我们上一节里讲到的,深 度缓冲区和模板缓冲区都是在Direct3D 初始化时顺手创建的。我们在之前讲解Direct3D 初始化时, 在11.4.4 节“ 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,指定AutoDepthStenciIFormat的深度缓冲区和模板缓冲区共同的像素格式。具体格式可以在结构体D3DFORMAT 中进行选取。下面列举一些可以选取的值:
D3DFMT_Dl6 深度缓存用16 位存储每个像素的深度值
D3DFMT_D24X8 深度援存用24 位存储每个像素的深度值
D3DFMT_D32 深度缉存用32 位存储每个像素的深度值
另外提一点,如果针对老掉牙的机器,在创建模极缓冲区之前,需要检查一下当前的设备是否支持我们稍后填进去的模板缓冲区格式。也就是在我们的“ Direct3D 初始化四步曲之二: 取信息”中取出信息来看一下我们的设备是否支持模板缓冲区格式, 用到的是CheckDeviceFormat 函数。由于计算机硬件技术的进步,市面上的显卡普遍功能都比较全面,对Direct3D 支持很好,很多时候
我们并不需要专门去做这一步。
20.2.2 清除模板缓冲区
清除模板缓冲区涉及到IDirect3DDevice9::Clear 这个方法。作为Direct3D 渲染五步曲里的领头羊,在之前的Direct3D 程序编写中已经被我们用过无数次了。这里我们故地重游一下,也讲出点新鲜的东西来。使用模板测试渲染每一帧之前, 都需要先清除上一帧保存在模板缓冲区中的模板值。而清除模板缓冲、颜色缓冲区以及深度缓冲区都是这个IDirect3DDevice9::Clear 方法的工作。
我们先贴出这个函数的原型:
HRESULT Clear(
[in] DWORD Count,
[in] const D3DRECT *pRects,
[in] DWORD Flags,
[in] D3DCOLOR Color,
[in] float Z,
[in] DWORD Stencil
);
首先我们附上在11.8.2 节“Direct3D 渲染五步曲之一: 清屏操作”中这个函数的讲解:
- 第一个参数,DWORD 类型的Count,指定了接下来的一个参数pRect 指向的矩形数组中矩形的数量。我们可以这样说, Count 和pRects 是一对好基友。如果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)就可以了,这里的RGB 为我们设定的三原色的值,都在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|D3DCLEAR_STENCIL, D3DCOLOR_XRGB(100, 150, 0), 1.0f, 0);
学到这里,这个三个缓冲区基本都介绍到了,我们之后的渲染五步曲的第一步就同时使用了这三个标识D3DCLEAR_STENCIL 、D3DCLEAR_TARGET 和D3DCLEAR_ZBUFFER 。
20.2.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;
这估计是我们这本书写到这里以来,第一次贴出这样不完整的数据结构来吧。下面我们对这些渲染状态中与模板相关的挨个进行讲解:
- D3DRS_STENClLENABLE :这个渲染状态用于启用或者禁用模板处理功能。这个参数指定为TRUE 表示启用模板处理; 指定为FALSE,则就表示禁用模板处理。
- D3DRS_STENCILFAIL : 这个渲染状态表示模板测试失败时进行的模板操作.而进行的模板操作默认为D3DSTENCILCAPS_KEEP。
- D3DRS_STENCILZFAIL: 该渲染状态表示模板测试通过时, 但是深度测试失败时进行的模板操作。默认的模板操作依旧是D3DSTENCILCAPS_KEEP 。
- D3DRS_STENCILPASS : 这个渲染状态表示模板测试通过时进行的模板操作。进行的模板操作默认依旧是为D3DSTENC1LCAPS_KEEP 。
- D3DRS_STENCILFUNC : 这个渲染状态可以指定用于模版测试的比较函数。比较函数可以是D3DCMPFUNC 枚举常量之一,该比较函数将通过模板掩码的模极参考值与模板缓冲区中当前像素的对应模板值比较, 如果为TRUE, 则通过模板测试。
- D3DRS_STENCILREF : 这个渲染状态用于设置模版参考值, 默认为0 。
- D3DRS_STENClLMASK : 这个渲染状态用于设置模板掩码,决定对模板参考值和模板缓冲区值的哪位进行比较, 默认掩码为
- 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;
下面表格对这些枚举类型中的成员进行说明:
对于目标表面上的每一个像素, 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;
我们还是用一个表格来说明:
20.2.4 对模板测试的一些理解
模板测试使用模板参考值、模板掩码、模板比较函数和当前像素在模板缓冲区中的模板值作为参数,判断某个像素是否将被写入到后台缓冲区中。模板测试的表达式是这样的:
其中的ref 表示模板参考值, mask 表示模板掩码, value 表示模板缓冲中的值, OP 表示模板比较函数,而符号“&”则表示模板值或模板参考值与模板掩码进行按位的与计算。
在Direct3D 进行模板测试前, 我们需要对模板测试的模板参考值、模板掩码和模板比较函数做一下设置。需要注意的是,模板参考值的默认值为0。当然,我们也可以自己设置,用的依然是那个号称万能的SetRenderState。第一个参数渲染状态设为
D3DRS_STENCILREF , 而第二个参数就填一个数值(最好是填16 进制的),表示需要的模版参考值。
举个小实例,下面这段代码就把模板参考值设为了1:
g_pd3dDevice->SetRenderState(D3DRS_STENCILREF, 0x1);
而模板掩码用于屏蔽模板参考值和当前测试像素的模板值的某些位,上文提到过,其默认值为0xffffffff, 表示不屏蔽任何位。而对应的0x000000 就表示屏蔽任何位。D3DRS_STENClLMASK与D3DRS_STENCILWRITEMASK 这两个渲染状态在SetRenderState 函数中就是分别表示模板掩码值和写掩码值的。
再举个小实例,下面这两句SetRenderState 就是在设置模板掩码值和写掩码值,用于屏蔽模板参考值和像素模板值的低十六位:
g_pd3dDevice->SetRenderState(D3DRS_STENCILMASK, 0xffffffff);
g_pd3dDevice->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff);
由于在实际编程中对不同的特效要在SetRenderState 中取不同的渲染状态,所以模板缓存很难总结出一个几步曲来,这个倒是有点可惜。如果上面这些知识看得不是很懂, 没关系,下面我们可以在实例代码中亲身体会一下。
我们来看看模板测试的一个非常重要的应用一一镜面特效。
20.3 镜面特效的实现
作者印象较深的是Dota2 中飘逸的英雄船长昆卡的技能洪流释放之后,在地上会留下一潭水,有小兵或者英雄路过的时候,这潭水就会倒影出来这些小兵或者英雄的镜像来,非常地逼真。
对了, Dota2 所用的引擎是Valve 公司最为著名的第一人称射击游戏《半条命2》系列所开发的Source 游戏引擎。这里顺带介绍一下,拓宽大家知识面。
Source 引擎也被我称为次世代引擎、起源引擎, 采用C++开发,跨Microsoft Windows 、Mac OS X 、Xbox 、Xbox360 、PlayStation 3 等众多平台。贴一张Source 引擎的Logo 吧:
先来看一下镜面成像的原理图:
而已知点q 的坐标,求出q’的坐标,就实现了我们镜面成像的目的。
其实,我们只要通过数学知识,求出q 点到q ’点的镜像变换矩阵就可以了,这样知道q 点,根据镜像变换矩阵,就可以求出q ’来。
这个镜像变换矩阵的求法,微软早就为我们准备好了,那就是D3DX 库中的D3DXMatrixReflect函数。我们在MSDN 中查到D3DXMatrixReflect 的声明如下:
D3DXMATRIX * D3DXMatrixReflect(
__inout D3DXMATRIX *pOut,
__in const D3DXPLANE *pPlane
);
- 第一个参数, D3DXMATRIX 类型的*pOut ,从类型上来看我们就知道他是一个D3DXMATRIX 类型的4 × 4 的矩阵,我们调用这个D3DXMatrixReflect 方法,其实就是在为这个矩阵赋值,通过Direct3D 的内部计算,让这个矩阵成为我们在第二个参数中提供的那个平面的镜像变换矩阵。
- 第二个参数, const D3DXPLANE 类型的 *plane , 显然就是一个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
D3DXMATRIX matReflect;
D3DXPLANEplane(0.0f, 1.0f, 1.0f, 0.0f) ; // 定义平面方程为y+z=O 的平面
D3DXMatrixReflect (&matReflect , &plane); // 计算y+z=O 平面的镜像变换矩阵
matWorld=matWorld * matReflect; // 镜像变换矩阵和原始世界矩阵相乘,得到镜像的世界矩阵
g_pd3dDevice->SetTransform(D3DTS_WORLD,& matReflect); //设置出镜像的世界矩阵
//接下来就写绘制镜像的代码就可以了
另外说明一点,在我们当前讲解的固定渲染流水线中,微软为我们把和数学与物理原理相关的
内容都封装起来了,很多时候,我们只要知道这些为我们封装好的函数如何使用,什么情况下使用
就好了,而不去深究具体的实现细节。作者认为这是很明智的选择,无形中大大降低了Direct3D
的入门难度。这又说明了学习Direct3D ,先学固定功能渲染流水线,再学可编程渲染流水线,是最为
科学、最轻松的学习路线。为了降低学习门槛,让文章更加贴近大众,通俗易’懂,我们也就暂时不深入讲解镜面成像的数学原理了。直接用这个D3DXMatrixReflect 方法就行了。
20.4 通过实例程序讲解
好了,镜面成像原理讲完了,我们接下来要着重看一下镜面特效的使用方法。对应镜面特效倒 是可以整出一个几步曲来介绍,下面的讲解为了更加清楚,我们结合了本节的配套源代码一起介绍。因为我们在讲的是渲染特效,所以代码精髓想都不用想,八九不离十就在Direct3D_Render()函数中。
在这个实例程序中,我们借助D3DXCreateBox 来快捷创建一个薄板作为镜子,然后从X 文件中载入一个3D人物并绘制出来,接着就顺理成章地以这个薄板作为镜子,在镜子中绘制出3D 人物模型的镜像。先放一张截图吧:
好吧,我们开始讲解。
1. 清空模板缓存
第一步,在清空模板缓存,并将模板缓存的值都设为0 ,用Clear 方法完成。这一步我们在Direct3D_Render()函数中渲染五步曲的第一步清屏里面己经做了,代码是这样:
// 【Direct3D渲染五步曲之一】:清屏操作
//--------------------------------------------------------------------------------------
g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL, D3DCOLOR_XRGB(100, 150, 0), 1.0f, 0);
2. 进行常规物体的绘制
这一步包含了渲染五步曲的第二步“开始绘制,,,以及第三步“ 正式绘制"。这一步代码基本上就是上一节介绍深度缓存时绘制人物模型和墙面的代码,没有什么新鲜的内容:
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之二】:开始绘制
//--------------------------------------------------------------------------------------
g_pd3dDevice->BeginScene(); // 开始绘制
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之三】:正式绘制
//--------------------------------------------------------------------------------------
D3DXMATRIX matHero,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); //绘制墙面
3. 启用模板缓存,以及对相关的绘制状态进行设置
调用一系列的方法来启用模板缓存,并且对模板比较函数、模板掩码以及更新模板缓存的渲染状态进行设置。用了一箩筐的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)代替模板缓存中的值。
4. 进行融合操作
在这一步中,我们关闭向深度缓存中写操作,然后启用融合操作。我们将源融合因子和目标融合因子分别指定为D3DBLEND_ZERO 和D3DBLEND_ONE 以防止对后台缓存进行更新。
// 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);
5. 确定出镜面区域
这一步主要就是绘制出镜面区域,也就是指定待会儿需要作为镜子的区域。因为之前将模板比较函数设置为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);
6 . 重新设置一系列渲染状态
确定好镜面区域后,下面就来重新设置一下之前被改过的渲染状态和融合状态,为后面马上将要进行的镜像绘制做准备。把深度缓存的写操作打开,设置比较函数为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);
7 . 计算镜像变换矩阵
这一步就是运用了我们在上面讲镜面特效时的思路,定义出镜面所在的平面的D3DXPLANE型平面,然后借助D3DXMatrixReflect 来得到镜像变换矩阵。
//7. 计算镜像变换矩阵
D3DXMATRIX matReflect;
D3DXPLANE planeXY(0.0f, 0.0f, 1.0f, 0.0f); // xy平面
D3DXMatrixReflect(&matReflect, &planeXY);
matWorld = matReflect * matHero;
8. 绘制镜像
忙了前面七步,就是为了现在不会吹灰之力地绘制出镜像。这一步完全没有技术含量,先设置一下世界矩阵,然后把第二步里面绘制物体的代码原封不动拷过来就行了:
//8.绘制镜子中的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); //绘制此部分
}
9. 恢复渲染状态
因为我们的Direct3D_Render()函数在消息循环的驱动下一直在被调用, 在绘制完镜像后, 需要把渲染状态调回来, 免得后面其他物体或者下一次调用Direct3D_Render() 函数时的渲染受到影响。这一步也就是关闭融合, 关闭模板测试,打开背面消隐:
// 9.恢复渲染状态
g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false);
g_pd3dDevice->SetRenderState( D3DRS_STENCILENABLE, false);
g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
20.5 示例程序D3Ddemo15
这个示例程序依旧是在D3Ddemo12 的基础上添油加醋,改造而成的,主要是Direct3D_Render() 函数的书写,代码在下面贴出。
//-----------------------------------【Direct3D_Render( )函数】-------------------------------
// 描述:使用Direct3D进行渲染
//--------------------------------------------------------------------------------------------------
void Direct3D_Render(HWND hwnd)
{
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之一】:清屏操作
//--------------------------------------------------------------------------------------
g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL, D3DCOLOR_XRGB(100, 150, 0), 1.0f, 0);
//定义一个矩形,用于获取主窗口矩形
RECT formatRect;
GetClientRect(hwnd, &formatRect);
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之二】:开始绘制
//--------------------------------------------------------------------------------------
g_pd3dDevice->BeginScene(); // 开始绘制
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之三】:正式绘制
//--------------------------------------------------------------------------------------
D3DXMATRIX matHero,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); //绘制墙面
//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);
// 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);
// 5.绘制出作为镜面的区域
D3DXMatrixTranslation(&matWorld, 0.0f, 0.0f, 0.0f);
g_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld);
g_pd3dDevice->SetMaterial(&g_MaterialsWall);
g_pMeshWall->DrawSubset(0);
// 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);
//7. 计算镜像变换矩阵
D3DXMATRIX matReflect;
D3DXPLANE planeXY(0.0f, 0.0f, 1.0f, 0.0f); // xy平面
D3DXMatrixReflect(&matReflect, &planeXY);
matWorld = matReflect * matHero;
//8.绘制镜子中的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); //绘制此部分
}
// 9.恢复渲染状态
g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false);
g_pd3dDevice->SetRenderState( D3DRS_STENCILENABLE, false);
g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之四】:结束绘制
//--------------------------------------------------------------------------------------
g_pd3dDevice->EndScene(); // 结束绘制
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之五】:显示翻转
//--------------------------------------------------------------------------------------
g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻转与显示
}
需要提醒大家一点,我们把观察点的位置改了一下,更利于观察镜面的效果了:
D3DXVECTOR3 vEye(100.0f, 0.0f, -250.0f); //摄像机的位置
另外,因为这个示例程序是对所有物体的镜面特效的实现做一个通用的实现,所以就没有针对这个3D 模型做相应的渲染效果的优化,也没有对更多的物理自然特性做调整,所以有可能还有一点点看起来不科学的地方,不过大体效果己经做出来了,无伤大雅。
我们放出运行的截图:
20.6 章节小憩
学完本章,我们已经从对游戏编程一无所知到可以写出这样带镜面特效的三维游戏人物演示场景出来, 算是一个不小的飞跃了吧。