在游戏三维场景中,想创造出唯美又具真实感的画面,常常需要绘制大量的物体。而这些物体之间通常都存在着遮挡的关系,离观察点较远的物体,会因为近处物体的遮挡而不可见,或者只有部分可见。在Direct3D 中,微软为我们提供了深度缓存(或称Z 缓存),配套着深度测试技术,来专门实现这种效果。下图是本篇文章配套程序的一个截图,可以发现,利用了深度测试,才能让战士与这个矩形墙面水乳交融,表达出这个战士似乎是从墙面中穿越而出的真实视觉效果。
所以,想要绘制出真实的游戏三维场景,深度测试便是必须之物。
19.1 形象化理解深度测试
首先来看一下对深度测试的形象化理解。把深度测试看做从一口井的井口向井中观望。把所有物体都赋予一个深度值,放到井中来显示。 深度越深的物体,离井口就越远。深度越浅的物体,离井口就越近。井口表面的深度值为0 。离井 口近而深度浅的物体,可能会把离井口远的物体遮挡住。最终显示在屏幕上的开启深度测试后的画 面,就如在井口处向井中观望里面物体显示出的遮挡与层次的效果一样。
当然,离井口的深度就是每个物体在世界坐标系中矩阵的Z 坐标值了。
接下来,我们看一下深度测试的具体概念。
19.2 深度测试相关概念讲解
而想要理解深度测试,首先需要理解深度缓冲区。深度缓冲区,也常常称为Z 缓存,是Direct3D 中用来存储绘制到屏幕上的每个像素点的深度 信息的一块内存缓冲区,是一个只含有特定像素深度信息而不含图像数据的表面,深度缓存为最终 绘制的图像中的每一个像素都保留了一个深度值。如果我们绘制屏幕的分辨率为800 × 600 像素的 话,那么深度缓存的大小也为800 × 600。
当Direct3D 将一个场景渲染到目标表面上时,它使用深度缓冲区来决定光栅化后各个多边形 的像素前后的遮挡关系,最终决定哪个颜色值会被绘制出来。
所以可以这样理解, Direct3D 通过比较当前绘制的像素点的深度和对应深度缓冲区的点的深度 值来决定是否绘制当前像素。如果深度测试的结果为TRUE,就会绘制当前像素, 并用当前像素点 的深度值来更新一下深度缓冲区,反之,则不予绘制。而在通常情况下,深度缓冲区对应于屏幕大 小的一块二维区域。
比如,我们在三维场景中同时绘制了一把长剑和一个盾牌两个立体模型。如果长剑的深度值为30 、盾牌的深度值为60 (比如取景变换中摄像机的位置矩阵在Z 轴上为负值D3DXVECTOR3 vEye(0.0f, 1.0, -100.0f) 、长剑模型的世界矩阵为( 0. 0f, 0.0f, 30.0f ) 、盾牌的世界矩阵为( 0.0f, 0.0f,60.0f) ) ,那么就是表示长剑的盾牌的前面。Direct3D 会首先绘制长剑和盾牌中的其一,而当绘制剩下的那个3D 模型的时候, Direct3D 会将当前3D 模型位于同一位置的像素与已经绘制的像素(如果两者在该位置都有像素的话)进行测试,若当前像素比原来的像素更近(即深度值更小),那么将用当前像素来更新掉原来的像素,否则不予更新。
当Direct3D 将一个场景渲染到目标表面的时候,它使用深度缓冲区决定光栅化后每个多边形的像素前后的遮挡关系,最终决定哪个颜色值被绘制出来, 对于一个启用了深度缓冲区的场景进行光栅化操作时,表面上的每个值都要进行深度测试。
19.3 深度测试使用四步曲
在Direct3D 中,使用深度测试有一个四步曲,但是这仅仅是一个按部就班的四步曲, 后三步 的顺序可以随意,只要在渲染前就可以了。而且由于Direct3D 默认开启深度测试,甚至不需要后 面三步,也可以在程序中畅通无阻地使用深度测试。但是, 我们本着学习的态度, 依然是要介绍一下这个知识点, 这样在实现某些特殊渲染效果的 时候才有“招”可用。就像现在有了游戏引擎, 想要快速做一款游戏的话, 完全可以去直接用现成 的游戏引擎。但是那毕竟是别人写出来的东西。如果本着学知识的态度的话, 我们应该了解引擎的 底层实现, 知其然并知其所以然。这样, 日积月累, 才会显得功力比那些做表层学问的人们来得深 厚,才能成为真正的技术大牛, 实现起游戏画面优化来游刃有余,甚至有能力写出属于自己的游戏 引擎。
深度测试使用四步曲如下:
- 创建深度缓冲区。
- 开启深度测试。
- 设直深度测试函数。
- 更新深度缓冲区。
下面我们分别来详细讲解。
1 . 四步曲之一: 创建深度缓冲区
关于深度缓冲区的创建, 因为是在Direct3D 初始化时顺手创建的,所以我们在之前讲解Direct3D 初始化时,在11.4.4 节就有提到。
回忆之前的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;
第十个参数,BOOL 类型的EnableAutoDepthStencil,表示Direct3D 是否为应用程序自动管理深度缓存,这个成员为TRUE的话,表示需要自动管理深度缓存,这时候就需要对下一个成员AutoDepthStencilFormat 进行相关像素格式的设置。
第十一个参数, D3DFORMAT 类型的AutoDepthStencilFormat,上面刚介绍过,如果我们把EnableAutoDepthStencil 成员设为TRUE 的话,在这里就需要指定AutoDeptbStencilFormat 的深度缓冲的像素格式。具体格式可以在结构体D3DFORMAT 中进行选取。我们列举一些可以选取的值:
D3DFMT_D16 深度缓存用16位存储每个像素的深度值
D3DFMT_D24X8 深度缓存用24位存储每个像素的深度值
D3DFMT_D32 深度缓存用32位存储每个像素的深度值
想要在之后绘制时使用深度测试的话,这里需要把第十个参数EnableAutoDeptbStencil 设为true , 表示让Direct3D 创建并自行管理一个深度缓冲区。而第十一个参数AutoDeptbStenciIFormat用于设置深度缓存的格式, 16 位、24 位、32 位等等格式任选。
2. 四步曲之二:开启深度测试
这一步的主角依旧是老朋友SetRenderState , 之前我们己经用过和介绍过无数遍了。因为这个函数的一个参数可以取各式各样的渲染状态和类型,所以注定了它是一个多面手。下面将要介绍的深度使用四步曲的后三步。
先看一下我们正在讲解的第二步,开启深度测试。自然就是给SetRenderState 两个参数取不同的值就可以了。将第一个参数设为D3DRS_ZENABLE , 表示第二个参数将对深度测试的开启或者关闭进行设置,第二个参数设为TRUE 或者FALSE ,表示开启或者关闭深度测试。
这一步的目的是开启深度测试,也就是这样写:
g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, true); //开启深度测试
当然,要关闭深度测试的话,把第二个参数取false 就可以了:
g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, false); //关闭深度测试
3. 四步曲之三:设置深度测试函数
还是那个神通广大的SetRenderState 函数。对应于这一步的任务,我们把它的第一个参数设为D3DRS_ZFUNC , 第二个参数设为想要进行使用的深度测试函数,在D3DCOMPFUNC 枚举类型中取值, 这个D3DCMPFUNC 枚举类型可以在MSDN 中查到:
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_LESS ,表示当测试点深度值小于深度缓冲区中相应值时,通过测试并绘制相关像素,这样没有被遮挡的物体才显示,被遮挡住的物体是不显示的。
这一步里面也就是一句SetRenderState 代码:
g_pd3dDevice->SetRenderState(D3DRS_ZFUNC, D3DCMP_LESS); //将深度测试函数设为D3DCMP_LESS
4 . 四步曲之四:更新深度缓冲区
配合第三步设置的深度测试函数,还需要设置深度测试成功时对深度缓冲区如何操作,是保持原来的深度值“按兵不动”呢,还是用当前像素的深度值来更新对应的数值“与时俱进”。
还是那个神通广大的SetRenderState 函数。对应于这一步的任务,我们把它的第一个参数设为D3DRS_ZWRITEENABLE ,表示在第二个参数里面将对深度缓冲区更改与否做出选择。第二个参数设为TURE 的话,表示深度测试成功之后,用当前像素的深度值更新深度缓冲区对应的数值。第二个参数设为TURE 是设置更新深度缓冲区时最常取的值,同时也是默认值。反之,第二个参数设为FALSE ,则表示尽管深度测试成功,还是不更新深度缓冲区对应的数值。
代码是这样写的:
g_pd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, true); //深度测试成功后,更新深度缓存
19.4 示例程序D3Ddemo14
示例程序的工程大体上和D3Ddemo12 差不多,依旧是四个文件。我们在这个程序中绘制了两个物体, 一个是章节开头中的那个帅气的战士,另外-个是一面巍 然不动的褐色的矩形墙,本来还可以把这面墙弄成真正的贴过图后的砖块墙的,但是那样未免会增 加一些代码量,增大大家理解的难度, 于是就用了这个D3DXCreateBox 函数来图个方便, 快捷绘 制一面单色的墙。
有变动的地方是Objects_lnit()函数中载入X 文件的名称变了一下, 然后是用如下的代码创建 了一个褐色的极薄的屏障:
//用D3DXCreateBox来创建一个极薄的屏障
D3DXCreateBox(g_pd3dDevice, 30.0f, 30.0f, 0.5f, &g_pMeshWall, NULL);
g_MaterialsWall.Ambient = D3DXCOLOR(0.8f, 0.2f, 0.1f, 1.0f);
g_MaterialsWall.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
g_MaterialsWall.Specular = D3DXCOLOR(0.1f, 0.1f, 0.1f, 1.0f);
然后Direct3D_Update() 函数中加入了对深度测试开与关进行控制的代码:
// 开启或者关闭深度测试
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);
我们可以用键盘上的数字键1 和2 在开启深度测试和关闭深度测试之间切换。
战士的初始世界矩阵Z 坐标为0.0f, 单色墙的世界矩阵Z 坐标为 -50.0f。所以开启深度测试后,战士在我们不动键盘鼠标让其移动的情况下,会被单色墙遮挡住。我们滑动鼠标上的滚轮,让战士的Z 坐标小于 -50.0f 之后, 会发现战士完全位于单色墙之前了,不再被其遮挡。
如果我们按下键盘上的2 键,关闭深度测试的话,会发现单色墙一直拦在战士的身前,战士本来披于身后被面部遮挡住的一头马尾金发辫, 竟然可以从正面看到, 非常地不科学。
接下来我们来看看一些开启/关闭深度测试的对比图。
开启了深度测试的状态 关闭了深度测试的状态
19.5 章节小憩
这是非常短小精悍的一章, 我们讨论了深度缓存和深度测试的方方面面。请大家记住, 在Direct3D中,深度测试默认是打开状态的,所以很多时候我们不需要专门去 设置。