本系列文章由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即可:
//***************************************************************************************** // //【Visual C++】游戏开发笔记系列配套源码四十六 浅墨DirectX教程十四 模板缓存与镜面特效专场 // VS2010版 // 2013年 3月3日 Create by 浅墨 //图标素材出处: VAMPIRE_SWEETIE //背景音乐素材出处:仙剑奇侠传3外传问情篇 // //***************************************************************************************** //***************************************************************************************** // Desc: 宏定义部分 //***************************************************************************************** #define SCREEN_WIDTH 800 //为窗口宽度定义的宏,以方便在此处修改窗口宽度 #define SCREEN_HEIGHT 600 //为窗口高度定义的宏,以方便在此处修改窗口高度 #define WINDOW_TITLE _T("【Visual C++】游戏开发笔记系列配套示例程序四十六 浅墨DirectX教程十四 模板缓存与镜面特效专场") //为窗口标题定义的宏 //***************************************************************************************** // Desc: 头文件定义部分 //***************************************************************************************** #include <d3d9.h> #include <d3dx9.h> #include <tchar.h> #include <time.h> #include "DirectInputClass.h" //***************************************************************************************** // Desc: 库文件定义部分 //***************************************************************************************** #pragma comment(lib,"d3d9.lib") #pragma comment(lib,"d3dx9.lib") #pragma comment(lib, "dinput8.lib") // 使用DirectInput必须包含的库文件,注意这里有8 #pragma comment(lib,"dxguid.lib") #pragma comment(lib, "winmm.lib") //***************************************************************************************** // Desc: 全局变量声明部分 //***************************************************************************************** LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; //Direct3D设备对象 LPD3DXFONT g_pTextFPS =NULL; //字体COM接口 LPD3DXFONT g_pTextAdaperName = NULL; // 显卡信息的2D文本 LPD3DXFONT g_pTextHelper = NULL; // 帮助信息的2D文本 LPD3DXFONT g_pTextInfor = NULL; // 绘制信息的2D文本 float g_FPS = 0.0f; //一个浮点型的变量,代表帧速率 wchar_t g_strFPS[50]={0}; //包含帧速率的字符数组 wchar_t g_strAdapterName[60]={0}; //包含显卡名称的字符数组 D3DXMATRIX g_matWorld; //世界矩阵 DInputClass* g_pDInput = NULL; //一个DInputClass类的指针 LPD3DXMESH g_pMesh = NULL; // 网格对象 D3DMATERIAL9* g_pMaterials = NULL; // 网格的材质信息 LPDIRECT3DTEXTURE9* g_pTextures = NULL; // 网格的纹理信息 DWORD g_dwNumMtrls = 0; // 材质的数目 LPD3DXMESH g_pMeshWall = NULL; // 墙面网格对象 D3DMATERIAL9 g_MaterialsWall; // 材质 //***************************************************************************************** // Desc: 全局函数声明部分 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ); HRESULT Direct3D_Init(HWND hwnd,HINSTANCE hInstance); HRESULT Objects_Init(); void Direct3D_Render( HWND hwnd); void Direct3D_Update( HWND hwnd); void Direct3D_CleanUp( ); float Get_FPS(); void Matrix_Set(); //***************************************************************************************** // Name: WinMain( ) // Desc: Windows应用程序入口函数 //***************************************************************************************** int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd) { //开始设计一个完整的窗口类 WNDCLASSEX wndClass = { 0 }; //用WINDCLASSEX定义了一个窗口类,即用wndClass实例化了WINDCLASSEX,用于之后窗口的各项初始化 wndClass.cbSize = sizeof( WNDCLASSEX ) ; //设置结构体的字节数大小 wndClass.style = CS_HREDRAW | CS_VREDRAW; //设置窗口的样式 wndClass.lpfnWndProc = WndProc; //设置指向窗口过程函数的指针 wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; //指定包含窗口过程的程序的实例句柄。 wndClass.hIcon=(HICON)::LoadImage(NULL,_T("icon.ico"),IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE); //从全局的::LoadImage函数从本地加载自定义ico图标 wndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); //指定窗口类的光标句柄。 wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH); //为hbrBackground成员指定一个灰色画刷句柄 wndClass.lpszMenuName = NULL; //用一个以空终止的字符串,指定菜单资源的名字。 wndClass.lpszClassName = _T("ForTheDreamOfGameDevelop"); //用一个以空终止的字符串,指定窗口类的名字。 if( !RegisterClassEx( &wndClass ) ) //设计完窗口后,需要对窗口类进行注册,这样才能创建该类型的窗口 return -1; HWND hwnd = CreateWindow( _T("ForTheDreamOfGameDevelop"),WINDOW_TITLE, //喜闻乐见的创建窗口函数CreateWindow WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, SCREEN_WIDTH, SCREEN_HEIGHT, NULL, NULL, hInstance, NULL ); //Direct3D资源的初始化,调用失败用messagebox予以显示 if (!(S_OK==Direct3D_Init (hwnd,hInstance))) { MessageBox(hwnd, _T("Direct3D初始化失败~!"), _T("浅墨的消息窗口"), 0); //使用MessageBox函数,创建一个消息窗口 } PlaySound(L"仙剑·战斗3.wav", NULL, SND_FILENAME | SND_ASYNC|SND_LOOP); //循环播放背景音乐 MoveWindow(hwnd,200,50,SCREEN_WIDTH,SCREEN_HEIGHT,true); //调整窗口显示时的位置,窗口左上角位于屏幕坐标(200,50)处 ShowWindow( hwnd, nShowCmd ); //调用Win32函数ShowWindow来显示窗口 UpdateWindow(hwnd); //对窗口进行更新,就像我们买了新房子要装修一样 //进行DirectInput类的初始化 g_pDInput = new DInputClass(); g_pDInput->Init(hwnd,hInstance,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE); //消息循环过程 MSG msg = { 0 }; //初始化msg while( msg.message != WM_QUIT ) //使用while循环 { if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) ) //查看应用程序消息队列,有消息时将队列中的消息派发出去。 { TranslateMessage( &msg ); //将虚拟键消息转换为字符消息 DispatchMessage( &msg ); //该函数分发一个消息给窗口程序。 } else { Direct3D_Update(hwnd); //调用更新函数,进行画面的更新 Direct3D_Render(hwnd); //调用渲染函数,进行画面的渲染 } } UnregisterClass(_T("ForTheDreamOfGameDevelop"), wndClass.hInstance); return 0; } //***************************************************************************************** // Name: WndProc() // Desc: 对窗口消息进行处理 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) //窗口过程函数WndProc { switch( message ) //switch语句开始 { case WM_PAINT: // 客户区重绘消息 Direct3D_Render(hwnd); //调用Direct3D_Render函数,进行画面的绘制 ValidateRect(hwnd, NULL); // 更新客户区的显示 break; //跳出该switch语句 case WM_KEYDOWN: // 键盘按下消息 if (wParam == VK_ESCAPE) // ESC键 DestroyWindow(hwnd); // 销毁窗口, 并发送一条WM_DESTROY消息 break; case WM_DESTROY: //窗口销毁消息 Direct3D_CleanUp(); //调用Direct3D_CleanUp函数,清理COM接口对象 PostQuitMessage( 0 ); //向系统表明有个线程有终止请求。用来响应WM_DESTROY消息 break; //跳出该switch语句 default: //若上述case条件都不符合,则执行该default语句 return DefWindowProc( hwnd, message, wParam, lParam ); //调用缺省的窗口过程来为应用程序没有处理的窗口消息提供缺省的处理。 } return 0; //正常退出 } //***************************************************************************************** // Name: Direct3D_Init( ) // Desc: 初始化Direct3D // Point:【Direct3D初始化四步曲】 // 1.初始化四步曲之一,创建Direct3D接口对象 // 2.初始化四步曲之二,获取硬件设备信息 // 3.初始化四步曲之三,填充结构体 // 4.初始化四步曲之四,创建Direct3D设备接口 //***************************************************************************************** HRESULT Direct3D_Init(HWND hwnd,HINSTANCE hInstance) { //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之一,创接口】:创建Direct3D接口对象, 以便用该Direct3D对象创建Direct3D设备对象 //-------------------------------------------------------------------------------------- LPDIRECT3D9 pD3D = NULL; //Direct3D接口对象的创建 if( NULL == ( pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) //初始化Direct3D接口对象,并进行DirectX版本协商 return E_FAIL; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之二,取信息】:获取硬件设备信息 //-------------------------------------------------------------------------------------- D3DCAPS9 caps; int vp = 0; if( FAILED( pD3D->GetDeviceCaps( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps ) ) ) { return E_FAIL; } if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT ) vp = D3DCREATE_HARDWARE_VERTEXPROCESSING; //支持硬件顶点运算,我们就采用硬件顶点运算,妥妥的 else vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING; //不支持硬件顶点运算,无奈只好采用软件顶点运算 //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之三,填内容】:填充D3DPRESENT_PARAMETERS结构体 //-------------------------------------------------------------------------------------- D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.BackBufferWidth = SCREEN_WIDTH; d3dpp.BackBufferHeight = SCREEN_HEIGHT; d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8; d3dpp.BackBufferCount = 2; d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE; d3dpp.MultiSampleQuality = 0; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow = hwnd; d3dpp.Windowed = true; d3dpp.EnableAutoDepthStencil = true; d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; d3dpp.Flags = 0; d3dpp.FullScreen_RefreshRateInHz = 0; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之四,创设备】:创建Direct3D设备接口 //-------------------------------------------------------------------------------------- if(FAILED(pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, vp, &d3dpp, &g_pd3dDevice))) return E_FAIL; //获取显卡信息到g_strAdapterName中,并在显卡名称之前加上“当前显卡型号:”字符串 wchar_t TempName[60]=L"当前显卡型号:"; //定义一个临时字符串,且方便了把"当前显卡型号:"字符串引入我们的目的字符串中 D3DADAPTER_IDENTIFIER9 Adapter; //定义一个D3DADAPTER_IDENTIFIER9结构体,用于存储显卡信息 pD3D->GetAdapterIdentifier(0,0,&Adapter);//调用GetAdapterIdentifier,获取显卡信息 int len = MultiByteToWideChar(CP_ACP,0, Adapter.Description, -1, NULL, 0);//显卡名称现在已经在Adapter.Description中了,但是其为char类型,我们要将其转为wchar_t类型 MultiByteToWideChar(CP_ACP, 0, Adapter.Description, -1, g_strAdapterName, len);//这步操作完成后,g_strAdapterName中就为当前我们的显卡类型名的wchar_t型字符串了 wcscat_s(TempName,g_strAdapterName);//把当前我们的显卡名加到“当前显卡型号:”字符串后面,结果存在TempName中 wcscpy_s(g_strAdapterName,TempName);//把TempName中的结果拷贝到全局变量g_strAdapterName中,大功告成~ if(!(S_OK==Objects_Init())) return E_FAIL; SAFE_RELEASE(pD3D) //LPDIRECT3D9接口对象的使命完成,我们将其释放掉 return S_OK; } HRESULT Objects_Init() { //创建字体 D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1000, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("Calibri"), &g_pTextFPS); D3DXCreateFont(g_pd3dDevice, 20, 0, 1000, 0, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"华文中宋", &g_pTextAdaperName); D3DXCreateFont(g_pd3dDevice, 23, 0, 1000, 0, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"微软雅黑", &g_pTextHelper); D3DXCreateFont(g_pd3dDevice, 26, 0, 1000, 0, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"黑体", &g_pTextInfor); // 从X文件中加载网格数据 LPD3DXBUFFER pAdjBuffer = NULL; LPD3DXBUFFER pMtrlBuffer = NULL; D3DXLoadMeshFromX(L"lighting.X", D3DXMESH_MANAGED, g_pd3dDevice, &pAdjBuffer, &pMtrlBuffer, NULL, &g_dwNumMtrls, &g_pMesh); // 读取材质和纹理数据 D3DXMATERIAL *pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer(); //创建一个D3DXMATERIAL结构体用于读取材质和纹理信息 g_pMaterials = new D3DMATERIAL9[g_dwNumMtrls]; g_pTextures = new LPDIRECT3DTEXTURE9[g_dwNumMtrls]; for (DWORD i=0; i<g_dwNumMtrls; i++) { //获取材质,并设置一下环境光的颜色值 g_pMaterials[i] = pMtrls[i].MatD3D; g_pMaterials[i].Ambient = g_pMaterials[i].Diffuse; //创建一下纹理对象 g_pTextures[i] = NULL; D3DXCreateTextureFromFileA(g_pd3dDevice, pMtrls[i].pTextureFilename, &g_pTextures[i]); } SAFE_RELEASE(pAdjBuffer) SAFE_RELEASE(pMtrlBuffer) //用D3DXCreateBox来创建一个极薄的镜子 D3DXCreateBox(g_pd3dDevice, 120.0f, 120.0f, 0.3f, &g_pMeshWall, NULL); g_MaterialsWall.Ambient = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); g_MaterialsWall.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); g_MaterialsWall.Specular = D3DXCOLOR(0.2f, 1.0f, 1.0f, 1.0f); // 设置光照 D3DLIGHT9 light; ::ZeroMemory(&light, sizeof(light)); light.Type = D3DLIGHT_DIRECTIONAL; light.Ambient = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Specular = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Direction = D3DXVECTOR3(1.0f, 1.0f, 1.0f); g_pd3dDevice->SetLight(0, &light); g_pd3dDevice->LightEnable(0, true); g_pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true); g_pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE, true); return S_OK; } //***************************************************************************************** // Name:Matrix_Set() // Desc: 设置世界矩阵 // Point:【Direct3D四大变换】 // 1.【四大变换之一】:世界变换矩阵的设置 // 2.【四大变换之二】:取景变换矩阵的设置 // 3.【四大变换之三】:投影变换矩阵的设置 // 4.【四大变换之四】:视口变换的设置 //***************************************************************************************** void Matrix_Set() { //-------------------------------------------------------------------------------------- //【四大变换之二】:取景变换矩阵的设置 //-------------------------------------------------------------------------------------- D3DXMATRIX matView; //定义一个矩阵 D3DXVECTOR3 vEye(100.0f, 0.0f, -250.0f); //摄像机的位置 D3DXVECTOR3 vAt(0.0f, 0.0f, 0.0f); //观察点的位置 D3DXVECTOR3 vUp(0.0f, 1.0f, 0.0f);//向上的向量 D3DXMatrixLookAtLH(&matView, &vEye, &vAt, &vUp); //计算出取景变换矩阵 g_pd3dDevice->SetTransform(D3DTS_VIEW, &matView); //应用取景变换矩阵 //-------------------------------------------------------------------------------------- //【四大变换之三】:投影变换矩阵的设置 //-------------------------------------------------------------------------------------- D3DXMATRIX matProj; //定义一个矩阵 D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4.0f,(float)((double)SCREEN_WIDTH/SCREEN_HEIGHT),1.0f, 1000.0f); //计算投影变换矩阵 g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &matProj); //设置投影变换矩阵 //-------------------------------------------------------------------------------------- //【四大变换之四】:视口变换的设置 //-------------------------------------------------------------------------------------- D3DVIEWPORT9 vp; //实例化一个D3DVIEWPORT9结构体,然后做填空题给各个参数赋值就可以了 vp.X = 0; //表示视口相对于窗口的X坐标 vp.Y = 0; //视口相对对窗口的Y坐标 vp.Width = SCREEN_WIDTH; //视口的宽度 vp.Height = SCREEN_HEIGHT; //视口的高度 vp.MinZ = 0.0f; //视口在深度缓存中的最小深度值 vp.MaxZ = 1.0f; //视口在深度缓存中的最大深度值 g_pd3dDevice->SetViewport(&vp); //视口的设置 } void Direct3D_Update( HWND hwnd) { //使用DirectInput类读取数据 g_pDInput->GetInput(); // 开启或者关闭深度测试 if (g_pDInput->IsKeyDown(DIK_1)) //按下1键,开启深度测试 g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, true); if (g_pDInput->IsKeyDown(DIK_2)) //按下2键,关闭深度测试 g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, false); // 按住鼠标左键并拖动,为平移操作 static FLOAT fPosX = 0.0f, fPosY = -1.5f, fPosZ = 0.0f; if (g_pDInput->IsMouseButtonDown(0)) { fPosX += (g_pDInput->MouseDX())* 0.08f; fPosY += (g_pDInput->MouseDY()) * -0.08f; } //鼠标滚轮,为观察点收缩操作 fPosZ += (g_pDInput->MouseDZ())* 0.02f; // 平移物体 if (g_pDInput->IsKeyDown(DIK_A)) fPosX -= 0.005f; if (g_pDInput->IsKeyDown(DIK_D)) fPosX += 0.005f; if (g_pDInput->IsKeyDown(DIK_W)) fPosY += 0.005f; if (g_pDInput->IsKeyDown(DIK_S)) fPosY -= 0.005f; D3DXMatrixTranslation(&g_matWorld, fPosX, fPosY, fPosZ); // 按住鼠标右键并拖动,为旋转操作 static float fAngleX = 0, fAngleY =0; if (g_pDInput->IsMouseButtonDown(1)) { fAngleX += (g_pDInput->MouseDY())* -0.01f; fAngleY += (g_pDInput->MouseDX()) * -0.01f; } // 旋转物体 if (g_pDInput->IsKeyDown(DIK_UP)) fAngleX += 0.05f; if (g_pDInput->IsKeyDown(DIK_DOWN)) fAngleX -= 0.05f; if (g_pDInput->IsKeyDown(DIK_LEFT)) fAngleY -= 0.05f; if (g_pDInput->IsKeyDown(DIK_RIGHT)) fAngleY += 0.05f; D3DXMATRIX Rx, Ry; D3DXMatrixRotationX(&Rx, fAngleX); D3DXMatrixRotationY(&Ry, fAngleY); g_matWorld = Rx * Ry * g_matWorld; Matrix_Set(); } //***************************************************************************************** // Name: Direct3D_Render() // Desc: 进行图形的渲染操作 // Point:【Direct3D渲染五步曲】 // 1.渲染五步曲之一,清屏操作 // 2.渲染五步曲之二,开始绘制 // 3.渲染五步曲之三,正式绘制 // 4.渲染五步曲之四,结束绘制 // 5.渲染五步曲之五,翻转显示 //***************************************************************************************** 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); //在窗口右上角处,显示每秒帧数 formatRect.top = 5; int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() ); g_pTextFPS->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_RGBA(0,239,136,255)); //显示显卡类型名 g_pTextAdaperName->DrawText(NULL,g_strAdapterName, -1, &formatRect, DT_TOP | DT_LEFT, D3DXCOLOR(1.0f, 0.5f, 0.0f, 1.0f)); // 输出绘制信息 formatRect.top = 30; static wchar_t strInfo[256] = {0}; swprintf_s(strInfo,-1, L"模型坐标: (%.2f, %.2f, %.2f)", matHero._41, matHero._42, matHero._43); g_pTextHelper->DrawText(NULL, strInfo, -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(135,239,136,255)); // 输出帮助信息 formatRect.left = 0,formatRect.top = 380; g_pTextInfor->DrawText(NULL, L"控制说明:", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(235,123,230,255)); formatRect.top += 35; g_pTextHelper->DrawText(NULL, L" 数字键1与2:开启或者关闭深度测试", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 按住鼠标左键并拖动:平移模型", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 按住鼠标右键并拖动:旋转模型", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 滑动鼠标滚轮:拉伸模型", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" W、S、A、D键:平移模型 ", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 上、下、左、右方向键:旋转模型 ", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" ESC键 : 退出程序", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之四】:结束绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->EndScene(); // 结束绘制 //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之五】:显示翻转 //-------------------------------------------------------------------------------------- g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻转与显示 } //***************************************************************************************** // Name:Get_FPS()函数 // Desc: 用于计算帧速率 //***************************************************************************************** float Get_FPS() { //定义四个静态变量 static float fps = 0; //我们需要计算的FPS值 static int frameCount = 0;//帧数 static float currentTime =0.0f;//当前时间 static float lastTime = 0.0f;//持续时间 frameCount++;//每调用一次Get_FPS()函数,帧数自增1 currentTime = timeGetTime()*0.001f;//获取系统时间,其中timeGetTime函数返回的是以毫秒为单位的系统时间,所以需要乘以0.001,得到单位为秒的时间 //如果当前时间减去持续时间大于了1秒钟,就进行一次FPS的计算和持续时间的更新,并将帧数值清零 if(currentTime - lastTime > 1.0f) //将时间控制在1秒钟 { fps = (float)frameCount /(currentTime - lastTime);//计算这1秒钟的FPS值 lastTime = currentTime; //将当前时间currentTime赋给持续时间lastTime,作为下一秒的基准时间 frameCount = 0;//将本次帧数frameCount值清零 } return fps; } //***************************************************************************************** // Name: Direct3D_CleanUp() // Desc: 对Direct3D的资源进行清理,释放COM接口对象 //***************************************************************************************** void Direct3D_CleanUp() { //释放COM接口对象 for (DWORD i = 0; i<g_dwNumMtrls; i++) SAFE_RELEASE(g_pTextures[i]); SAFE_DELETE(g_pTextures); SAFE_DELETE(g_pMaterials); SAFE_DELETE(g_pDInput); SAFE_RELEASE(g_pMeshWall); SAFE_RELEASE(g_pMesh); SAFE_RELEASE(g_pd3dDevice); SAFE_RELEASE(g_pTextAdaperName) SAFE_RELEASE(g_pTextHelper) SAFE_RELEASE(g_pTextInfor) SAFE_RELEASE(g_pTextFPS) SAFE_RELEASE(g_pd3dDevice) }
本篇文章的配套程序中依旧可以用键盘上的数字键1和2在开启深度测试和关闭深度测试之间切换,不过建议不要去关闭深度测试,不然会被毁三观的。。。关了深度测试后的冷美人“雷霆”丑得惨不忍睹。。。。
因为上面已经把最主要最精髓的Direct3D_Render()函数大卸九块了,这个示例程序大家理解起来应该就不是什么问题了。需要提醒大家一点,我们把观察点的位置改了一下,更利于观察镜面的效果了。
D3DXVECTOR3 vEye(100.0f, 0.0f, -250.0f); //摄像机的位置
另外,这个事例程序中因为是在对所有物体的镜面特效的实现做一个笼统的代表,所以就没有针对这个3D模型做相应的渲染效果的优化,所以有可能还有一点点看起来不科学的地方,不过无伤大雅的。
下面放出示例程序的截图:
这个示例程序中的背景音乐用的是《仙剑奇侠传3问情篇》中的一首战斗音乐,有些小闹腾的。
文章最后,依旧是放出本篇文章配套源代码的下载:
本节笔记配套源代码请点击这里下载:
以上就是本节笔记的全部内容,更多精彩内容,且听下回分解。
浅墨在这里,希望喜欢游戏开发系列文章的朋友们能留下你们的评论,每次浅墨登陆博客看到大家的留言的时候都会非常开心,感觉自己正在传递一种信仰,一种精神。
文章最后,依然是【每文一语】栏目,今天的句子是:
想一千次,不如去做一次。华丽的跌倒,胜过无谓的徘徊。
下周一,让我们离游戏开发的梦想更近一步。
下周一,游戏开发笔记,我们,不见不散。