网络游戏客户端编程
学习目标
1.掌握模板的概念
2.掌握模板的使用方法
3.掌握阴影体的概念
4.掌握实时阴影的技术实现
知识背景
1.掌握D3D程序的基本框架
2.掌握D3D基本图形的绘制
本章要点
1.模板缓冲
2.阴影体
3.实时阴影的技术实现
引 言
在现在的3D游戏中,有一项特效是非常重要的,那就是阴影。可以说阴影是游戏仿真度的一个非常重要的指标。早期的3D游戏,阴影技术比较简单,一般是根据角色的形状,建立一张2D的阴影图片,在游戏中直接把图片贴在人物的脚下。这样的阴影不会随着人物的运动而改变,可以说这只是一种简单的仿真。随着3D图形技术的不断发展,阴影技术也得到了提高,现在一种比较流行的阴影实现技术是利用模板缓冲区来实现的。本章将主要学习如何从模板缓冲区入手,创建游戏中的实时阴影。
7.1 模板技术
7.1.1 模板的概念
模板缓冲(Stencil Buffer)用来控制是否一个像素一个像素地向渲染目标表面绘制场景。在模板缓冲最基本的一级上,它能够使程序掩盖住被渲染图像的一部分,使它不能显示。
模板缓冲区是一个记录每个像素信息的附加的缓冲区,很像一个z缓冲区。实际上,该缓冲区就驻留在z缓冲区的某些位中。常见的模板/z缓冲区格式为15位的z和1位的模板,或24位的z和8位的模板,如图7-1如示。在描绘多边形时,可以针对每个像素,对模板的内容进行简单的数学操作。例如,可以增加或减少模板缓冲区,或在模板值没能通过一项简单的比较测试时,拒绝绘制像素。可以将帧缓冲区的一个区域标出,然后只对标出(或未标出)的区域进行描绘。上述操作对于此类效果十分有用,各种体积效果就是很好的例子,比如阴影量。
图7-1 模板的存储格式
模板缓冲的工作原理也类似于深度缓冲。模板缓冲发挥作用的时机是在所有的3D模型已投影到了目标缓冲区后。这样做的原因是模板会对每一个将要绘制的像素进行模板测试,模板测试使用模板参考值、模板掩码、模板比较函数,以及当前像素对应的模板缓冲区的模板值作为参数。在测试后,会根据测试结果对模板缓冲中的值做出适当的处理。最后在渲染出最终的效果时,系统会根据模板缓冲的值做出适当的渲染效果。可以看出,模板缓冲技术的本质和深度缓冲比较类似,都是提供一个标准进行比较,从而决定是否显示某个像素。其工作过程可参见图7-2。
Direct3D在模板缓冲上执行一个基于pixel-by-pixel的测试。对于目标表面上的每一个像素,它使用模板缓冲中相应的值——模板参考值(stencil reference value)和模板掩模值(stencil mask value)来执行测试。如果测试通过,Direct3D就会执行一个动作。测试使用下面的步骤来进行:
(1)将模板参考值与模板掩模进行逐位AND运算;
(2)将当前像素的模板缓冲置于模板掩模进行逐位AND运算;
(3)用比较函数比较前两步得到的结果。
如果写成伪代码形式,步骤如下:
(StencilRef & StencilMask) CompFunc
(StencilBufferValue & StencilMask)
上式中,StencilBufferValue是当前像素的模板缓冲的内容;伪代码使用&符号来表示逐位AND操作;StencilMask表示模板掩模的值;StencilRef表示模板参考值;CompFunc是比较函数。
图7-2 模板的工作过程
如果模板测试通过了,那么当前像素就会被写到目标表面;如果没有通过,则会将当前像素忽略掉。默认的比较行为是无论每个逐位操作得到什么结果,都执行写像素操作(D3DCMP_ALWAYS标志)。可以改变D3DRENDERSTATE_STENCILFUNC渲染状态来改变这一行为,通过传递D3DCMPFUNC枚举类型的一个成员来声明所需的比较函数。
7.1.2 模板的程序实现
1.清空模板缓冲
先清空模板缓冲,使得整个模板缓冲的内容为一个指定值,例如:
Device->Clear(0, 0,
D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER | D3DCLEAR_STENCIL,
0xff000000, 1.0f, 0 );
D3DCLEAR_STENCIL标记说明在清空时要把模板绶冲区也清空,而用什么值来清空则是由最后一个参数来说明的,一般是设为0。
2.启用模板缓冲
在默认情况下,D3D中的模板缓冲是关闭的,这是因为模板缓冲技术是一个逐像素比较的操作,需要占用一定的时间,而这个功能并不是每时每刻都需要使用的。要想使用这个功能,需要如下面这样操作来打开使用开关。
Device->SetRenderState(D3DRS_STENCILENABLE, TRUE);
3.在模板缓冲中标记需要绘制的区域
这里要涉及模板测试,然后更新模板缓冲入口(value)。模板测试工作是由系统来做的,但它的测试标准是由程序来指定的。
模板测试:
( ref & mask ) ComparisonOperation ( value & mask )
模板测试是针对每个像素的,不过先假定模板是开启的(enabled),而且它获得两个操作数:
(1)左边的操作数(LHS = ref & mask),表明应用程序定义的模板参考值(ref)和应用程序定义的掩码值(mask)的与操作;
(2)右边的操作数(RHS = value & mask)表明模板缓冲中将要测试的特定像素(value)和应用程序定义的掩码值(mask)的与操作。
模板测试将通过指定的比较运算来比较LHS和RHS,这整个表达式的最终结果是一个布尔值(true或false)。如果测试结果为true,将把像素写入后台缓冲,如果是false,就阻止像素的写入。当然,如果像素没有被写入后台缓冲,那么它也不会被写入深度缓冲。
模板参考值(Stencil Reference Value)ref默认为0,但是可以通过D3DRS_ STENCILREF渲染状态来改变它。
例如,以下的代码设置模板参考值为1:
Device->SetRenderState ( D3DRS_STENCILREF,0x1 );
模板掩码值mask用来遮盖ref和value变量的位。默认的mask为0xffffffff,它不会掩盖任何位。可以通过设置D3DRS_STENCILMASK渲染状态来改变mask。以下例子表明掩盖高16位:
Device->SetRenderState(D3DRS_STENCILMASK,0x0000ffff);
测试函数ComparisonOperation可以采用的方案应该是一个枚举值:
typedef enum _D3DCMPFUNC {
D3DCMP_NEVER = 1, //模板测试永远不能成功
D3DCMP_LESS = 2, //如果LHS < RHS的话,模板测试成功
D3DCMP_EQUAL = 3, //如果LHS = RHS,模板测试成功
D3DCMP_LESSEQUAL = 4, //如果LHS <= RHS 模板测试成功
D3DCMP_GREATER = 5, //如果LHS>RHS 模板测试成功
D3DCMP_NOTEQUAL = 6, //如果LHS !=RHS模板测试成功
D3DCMP_GREATEREQUAL = 7, //如果LHS >= RHS模板测试成功
D3DCMP_ALWAYS = 8, //模板测试永远成功
D3DCMP_FORCE_DWORD = 0x7fffffff
} D3DCMPFUNC;
4.更新模板缓冲
除了决定是否写入或阻止一个特定像素被写入后台缓冲,还能通过三种方法来定义模板缓冲入口将如何被更新。
1)[i][j]处像素模板缓冲测试失败
可以设置D3DRS_STENCILFAIL渲染状态来定义如何更新模板缓冲中的[i][j]来响应这种情况:
Device->SetRenderState(D3DRS_STENCILFAIL,StencilOperation);
2)[i][j]处像素深度缓冲测试失败
可以设置D3DRS_STENCILZFAIL渲染状态来定义如何更新[i][j]入口以响应这种情况:
Device->SetRenderState(D3DRS_STENCILZFAIL,StencilOperation);
3)[i][j]处像素深度缓冲和模板缓冲测试均成功
可以设置D3DRS_STENCILPASS渲染状态来定义如何更新[i][j]入口以响应这种情况:
Device->SetRenderState( D3DRS_STENCILPASS,StencilOperation);
其中StencilOperation可以是如表7-1所示的预定义常数之一。
表7-1 模板操作的类型
值 | 意 义 |
D3DSTENCILOP_KEEP | 指定不改变模板缓冲 |
D3DSTENCILOP_ZERO | 指定设置模板缓冲为0 |
D3DSTENCILOP_REPLACE | 指定用模板引用值来代替模板缓冲 |
D3DSTENCILOP_INCRSAT | 指定增加模板缓冲入口. 如果增加后的值大于最大值,就把它限定为那个最大值 |
D3DSTENCILOP_DECRSAT | 指定减少模板缓冲入口. 如果减少后的值小于0,就把它限定为0 |
D3DSTENCILOP_INVERT | 指定取模板缓冲入口的逆 |
D3DSTENCILOP_INCR | 指定增加模板缓冲入口. 如果增加后的值大于最大值,把它限定为0 |
D3DSTENCILOP_DECR | 指定减少模板缓冲入口. 如果减少后的值小于0,就把它限定为无穷大 (maximum allowed value.) |
7.1.3 模板的简单应用
1.模板技术的应用场合
模板技术由于其独特的性质,在一定条件下使用起来可以达到一些意想不到的好处。它可以发挥作用的地方如下。
1)渐隐、淡入淡出与Swipe
渐隐技术就是将一幅图像逐渐地用另一幅图像替换掉。可以使用Direct3D的多纹理融合技术来达到这样的效果,但是一般还是选用模板缓冲来完成。这样,就能在使用模板缓冲进行渐隐处理时,仍然可以使用多纹理融合来进行其他效果的处理。
当程序执行渐隐操作时,要渲染两个不同的图像。这时,使用模板缓冲来控制将哪幅图像的像素绘制到渲染目标表面。可以定义一系列模板掩模,并将它们拷贝到模板缓冲中。另一方面,也可以为第一帧定义一个基本的模板掩模,然后随着帧的变化,再对基本掩模进行适当的改变。
在渐隐操作开始时,设置的模板函数和模板掩模应该使开始图像的大部分像素能够通过测试,而终止图像的大部分像素则不应该通过测试。对于连续的帧,模板掩模要不断变化,这样才能使能够通过测试的开始图像的像素不断减少,而随着帧的变化,越来越多的终止图像的像素将能够通过测试。通过这样的处理,就可以使用任意的图像来实现渐隐效果了。
淡入淡出实际上是渐隐技术的一种特殊情况。淡入技术就是由一幅黑色或白色的图像逐渐变换为一幅场景中的图像。淡出则刚好相反,它是由一个场景图像逐渐变换为黑色或白色。
Direct3D程序还可以使用另一种类似的技术——swipe。例如,如果执行了一个从右至左的swipe操作,最终的图像就会逐渐从右至左地滑出到开始图像的上面。与执行渐隐操作时一样,需要定义一些列的模板掩模,并将这些掩模加载到连续变化的帧的模板缓冲中;或者,可以对开始模板掩模进行连续的调制,再将调制后的掩模值应用到图像上。用这些模板掩模来控制开始图像和终止图像的像素的填写,从而实现预想的效果。
一个swipe操作要比一个渐隐操作更加复杂,因为它要按照与swipe相反的顺序来读取像素。也就是说,当一个swipe操作从右到左执行时,那么程序就必须在终止图像中从左到右地读取像素。
2)贴纸
Direct3D程序用贴纸技术将一个特殊图元图像中的像素绘制到渲染目标表面。可以对具有共面多边形的图元使用这一技术,这样可以保证它们能够被正确地渲染。
比如要把一些轮胎痕迹及黄色警戒线渲染到一条公路上。这些痕迹会直接位于公路的表面上,痕迹的z值与公路的z值完全相同。这样,深度缓冲就不能清楚地将它们两者分离开来,位于后面的图元上的一些像素就会被渲染到前面的图元上。最终的图像就会在帧与帧之间产生微弱的闪光。这种效果称为“z fighting”或“flimmering”。
要解决这一问题,就要使用一个模板来将贴纸出现地方的后面图元的像素掩盖掉。然后,关闭z-buffering,将前面图元的图像渲染到目标表面上被掩盖掉的区域上。
也可以使用多纹理融合来解决这一问题,但是,这样做就会限制其他使用多纹理融合才能实现的效果的数量。而使用模板缓冲就可以节省多纹理融合的能力,从而能够实现更多的特殊效果。
3)合成
可以使用模板缓冲来将2D或3D图像合成到一个3D场景中。用模板缓冲中的一个掩模来封闭渲染目标表面上的某个区域,然后就可以将2D信息,例如文本或位图,填写到被封闭的区域。另一方面,程序还可以在渲染目标表面上被模板掩模掉的区域绘制额外的3D图元,甚至可以绘制一个完整的场景。
游戏中经常将多个3D场景合成在一起。例如,驾驶类游戏中通常要显示一个后视镜,镜中显示了驾驶员背后的3D场景,这时,就可以用合成技术来将后视的三维场景合成到后视镜中。如图7-3所示。
图7-3 用模板实现合成的效果
4)轮廓与剪影
模板缓冲还能够用来实现一些抽象的效果,如轮廓与剪影。
如果将一个模板掩模应用到一个与图元有相同形状但是尺寸小一些的图像上,那么最终的图像就会得到这个图元的轮廓。然后,可以在模板掩模的区域填充一个具有固定颜色的图像,从而得到一种类似于浮雕的效果。
如果模板掩模的大小和形状与图元的大小和形状完全一样,那么最后的图像就会在图元存在的地方出现一个“洞”。可以将这个“洞“填充上黑色,这样就得到了这个图元的剪影效果。
2.实现模板深度
下面通过一个具体的例子来说明如何在程序中使用模板实现一些特殊的效果。在这个例子中,通过模板缓冲来记录一个模型的层次深度,并用不同的颜色来表示不同的深度,效果如图7-4所示。
图7-4 用模板显示模型重叠关系
【例7-1】 用模板显示模型重叠关系:
VOID Render()
{
// 清空后备缓冲和深度缓冲
g_pd3dDevice->Clear( 0, NULL,
D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(0,0,0), 1.0f, 0 );
g_pd3dDevice->BeginScene();
SetStatesForRecordingDepthComplexity();
SetupMatrices();
g_pMesh.Render(g_pd3dDevice);
ShowDepthComplexity();
g_pd3dDevice->EndScene();
g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
}
在模板深度效果的这个例子中,按通常的程序框架建立一个程序并在其中导入一个直升机的模型,通过对时间的响应,直升机会在划定的区域做盘旋飞行。这个例子的核心程序部分是在Render()函数中完成的,下面说明一下函数Render()。
先清空所有的缓冲区,包括目标缓冲区、深度缓冲区和模板缓冲区,然后开始绘图。通过调用函数SetStatesForRecordingDepthComplexity()开始做模板深度的前期准备工作。在SetupMatrices()中完成对时间的响应,并把时间的变化转化为直升机飞行位置的变化。接着调用直升机模型的演示函数。最后通过ShowDepthComplexity()函数与前面的模板设置相对应,完成模板的所有设置。
将这段代码中最重要的两个函数做详细的介绍如下。
SetStatesForRecordingDepthComplexity()函数用于在渲染前对模板操作做相应的设置。
【例7-2】 在渲染前对模板进行设置:
VOID SetStatesForRecordingDepthComplexity()
{
// 清空模板缓冲
Device->Clear( 0L, NULL, D3DCLEAR_STENCIL, 0x0, 1.0f, 0L );
// 执行模板测试, 并打开模板功能开关
Device->SetRenderState( D3DRS_STENCILENABLE, TRUE );
// 将模板测试函数设为总是成功
Device->SetRenderState( D3DRS_STENCILFUNC, D3DCMP_ALWAYS );
// 设置模板参考值为0
Device->SetRenderState( D3DRS_STENCILREF, 0 );
// 模板掩码设为0
Device->SetRenderState( D3DRS_STENCILMASK, 0x00000000 );
// 设置可以写到模板缓冲的写入掩码值
Device->SetRenderState( D3DRS_STENCILWRITEMASK,0xffffffff );
// 对模板缓冲中的每个像素做增量操作,
//深度测试失败对模板缓冲中的值做增量操作
Device->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_INCRSAT );
//模板测试失败,保持模板缓冲中的值
Device->SetRenderState( D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP );
// 模板测试成功,对模板缓冲中的值做增量操作
Device->SetRenderState( D3DRS_STENCILPASS,D3DSTENCILOP_INCRSAT );
}
ShowDepthComplexity()函数用于模型渲染完后对模板所做的操作。
【例7-3】 在渲染后对模板进行设置:
VOID ShowDepthComplexity()
{
// 在离开缓冲后,打开透明混合
Device->SetRenderState( D3DRS_ZENABLE, FALSE );
Device->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE );
Device->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCCOLOR );
Device->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCCOLOR );
// 设置模板操作的状态,深度测试失败,模板测试成功与失败这几种情况下都不改变
//模板缓冲中的值。左操作数等于右操作数时视为模板测试成功, 模板参考值设为0
Device->SetRenderState( D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP );
Device->SetRenderState( D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP );
Device->SetRenderState( D3DRS_STENCILPASS, D3DSTENCILOP_KEEP );
Device->SetRenderState( D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL );
Device->SetRenderState( D3DRS_STENCILREF, 0 );
// 设置目标缓冲区为黑色
Device->Clear( 0L, NULL, D3DCLEAR_TARGET, 0x00000000, 1.0f, 0L );
// 设置渲染状态用于视口区
// 这个区域的颜色将通过入口
D3DRS_TEXTUREFACTOR
Device->SetFVF( D3DFVF_XYZRHW );
Device->SetStreamSource( 0, g_pBigSquareVB, 0, sizeof(D3DXVECTOR4) );
Device->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TFACTOR );
Device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 );
// 把模板缓冲中第一位有值的区域(即没有重叠的区域)设为红色
Device->SetRenderState( D3DRS_STENCILMASK, 0x01 );
Device->SetRenderState( D3DRS_TEXTUREFACTOR, 0xffFF0000 );
Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );
//把模板缓冲中第二位有值的区域(即有重叠的区域)设为绿色
Device->SetRenderState( D3DRS_STENCILMASK, 0x02 );
Device->SetRenderState( D3DRS_TEXTUREFACTOR, 0xff00ff00 );
Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );
//把模板缓冲中第三位有值的区域(即有多层重叠的区域)设为蓝色
Device->SetRenderState( D3DRS_STENCILMASK, 0x04 );
Device->SetRenderState( D3DRS_TEXTUREFACTOR, 0xff0000FF );
Device->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );
//在所有的渲染完成后,恢复原来的正常状态
//为第二次的渲染保留环境
Device->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
Device->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
Device->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_MODULATE );
Device->SetRenderState( D3DRS_ZENABLE, TRUE );
Device->SetRenderState( D3DRS_STENCILENABLE, FALSE );
Device->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE );
}
经过这一系列的设置操作,模型就会按不同的模板深度显示出不同的颜色。重要的是要注意模板设置的步骤和次序,在渲染前设置模板测试的规则,在渲染后根据模板值做相应的操作,以达到想要的效果