一 模板的概念
模板缓存(Stencil Buffer)用来控制是否一个像素一个像素地向渲染目标表面绘制场景。在模板缓存最基本的一级上,它能够使程序掩盖住被渲染图像的一部分,使它不能显示。
模板缓存区是一个记录每个像素信息的附加的缓存区,很像一个Z缓存区。实际上,该缓存区就驻留在Z缓存区的某些位中。常见的模板/Z缓存区格式为15位的Z缓存和1位的模板缓存,或24位的Z缓存和8位的模板缓存。在描绘多边形时,可以针对每个像素,对模板的内容进行简单的数学操作。例如,可以增加或减少模板缓存区,或在模板值没能通过一项简单的比较测试时,拒绝绘制像素。可以将帧缓存区的一个区域标出,然后只对标出(或未标出)的区域进行绘制。上述操作对于此类效果十分有用,各种体积效果就是很好的例子,比如阴影量。
模板缓存的工作原理也类似于深度缓存。模板缓存发挥作用的时机是在所有的3D模型已投影到了目标缓冲区后。这样做的原因是模板会对每一个将要绘制的像素进行模板测试。模板测试使用模板参考值、模板掩码、模板比较函数,以及当前像素对应的模板缓存区的模板值作为参考。在测试后,会根据测试结果对模板缓存中的值做出适当的处理。最后在渲染出最终的效果时,系统会根据模板缓存的值作为适当的渲染效果。可以看出,模板缓存技术的本质和深度缓存比较类似,都是提供一个标准进行比较,从而决定是否显示某个像素。
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是比较函数。
二 模板的程序实现
1、 清空模板缓存
先清空模板缓存,使得整个模板缓存的内容为一个指定值,例如:
Device->Clear(0,0,D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL,0xff,000000,1.0f,0);
D3DCLEAR_STENCIL标记说明在清空时要把模板缓存区也清空,而用什么值来清空则是由最后一个参数值来说明的,一般是设为0。
2、 启动模板缓存
在默认情况下,D3D的模板缓存是关闭的,这是因为模板缓存技术是一个逐像素比较操作,需要占用一定的时间,而这个功能并不是每时每刻都需要使用的。要想使用这个功能,需要如下操作来打开开关。
Device->SetRenderState(D3DRS_STENCILENABLE,TRUE);
3、 在模板缓存中标记需要绘制的区域
这里要涉及模板测试,然后更新模板缓存入口(value)。模板测试工作由系统来做的,但它的测试标准是由程序来指定的。
模板测试:
(ref&mask)ComparisionOperation(value&mask)
模板测试是针对每一个像素的,不过先假定模板是开启的,而且它获得两个操作数:
(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,0x);
模板掩码值mask用来遮盖ref和value变量的位。默认的mask为0xffffffff,它不会掩盖任何位。可以通过设置D3DRS_STENCILMASK渲染状态来改变mask.。以下例子表示掩盖高16位:
Device->SetRenderState(D3DRS_STENCILMASK,0x0000ffff);
测试函数ComparisionOperation可以采用的方案应该是一个枚举值:
typedef enum _D3DCMPFUNC {
D3DCMP_NEVER=1, //模板测试永远不会成功
D3DCMP_LESS=2,
D3DCMP_EQUAL=3,//如果LHS=RHS,模板测试成功
D3DCMP_LESSQUAL=4,
D3DCMP_GREATER=5,
D3DCMP_NOTEQUAL=6,
D3DCMP_GREATEREQUAL=7,
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_STENCILXZFAIL渲染状态来定义如何更新[i][j]入口以响应这种情况:
Device->SetRenderState(D3DRS_STENCILZFAIL,StencilOperation);
3)[i][j]处像素深度缓存和模板缓存测试均成功
可以设置D3DRS_STENCILPASS渲染状态来定义如何更新[i][j]入口以响应这种情况:
Device->SetRenderState(D3DRS_STENCILPASS,StencilOperation);
其中StencilOperation可以是如下预定义常数之一
模板操作的类型:
D3DSTENCILOP_KEEP 指定不改变模板缓存
D3DSTENCILOP_ZERO指定设置模板缓存为0
D3DSTENCILOP_REPLACE 指定用模板引用值来代替模板缓存
D3DSTENCILOP_INCRSAT 指定增加模板缓存入口,如果增加后的值大于最大值,就把它限定为最大值
D3DSTENCILOP_DECRSAT 指定减少模板缓存入口,如果减少后的值小于0,就把它限定为0
D3DSTENCILOP_INVERT 指定取模板缓存入口的逆
D3DSTENCILOP_INCR 指定增加模板缓存入口。如果增加后的值大于最大值,就把它限定为无穷大(maximum allowed value)
三 模板的简单应用
1、 模板技术的应用场合
模板技术由于其独特的性质,在一定条件下使用起来可以达到一些意想不到的好处。它可以发挥作用的地方如下:
1) 渐隐技术、淡入淡出与Swipe
渐隐技术就是将一副图像逐渐地用另一幅图像替换掉。可以使用Direct3D的多纹理整合技术来达到这样的效果,但是一般还是选用模板缓存来完成。这样,就能在使用模板缓存进行渐隐处理时,仍然可以使用多纹理整合来进行其他效果的处理。
当程序执行渐隐操作时,要渲染两个不同的图像。这时,使用模板缓存来控制将哪幅图像的像素绘制到渲染目标表面。可以定义一系列模板掩模,并将它们拷贝到模板缓存中。另一方面,也可以为第一帧定义一个基本的模板掩模,然后随着帧的变化,再对基本掩模进行适当的改变。
在渐隐操作开始时,设置的模板函数和模板掩模应该使开始图像的大部分像素能够通过测试,而终止图像的大部分像素则不应该通过测试。对于连续的帧,模板掩模要不断变化,这样才能使能够通过测试的开始图像的像素不断减少,而随着帧的变化,越来越多的终止图像的像素将能够通过测试。通过这样的处理,就可以使用任意的图像来实现渐隐效果了。
淡入淡出实际上是渐隐技术的一种特殊情况。淡入技术就是由一幅黑色或白色的图像逐渐变换为一幅场景中的图像。淡出则刚好相反,它是由一个场景图像逐渐变换为黑色或白色。
Direct3D程序员还可以使用另一种类似的技术-swip。例如,如果执行了一个从右至左的swipe操作,最终的图像就会逐渐从右至左地滑出到开始图像的上面。与执行渐隐操作时一样,需要定义一系列的模板掩模,并将这些掩模加载到连续变化的帧的模板缓存中;或者,可以对开始模板掩模进行连续的调制,再将调制后的掩模值应用到图像上。用这些模板掩模来控制开始图像和终止图像的像素的填写,从而实现预想的效果。
一个swipe操作要比一个渐隐操作更加复杂,因为它要按照与swipe相反的顺序来读取像素。也就是说,当一个swipe操作从右到左执行时,那么程序就必须在终止图像中从左到右的读取像素。
2) 贴纸
Direct3D程序用贴纸技术将一个特殊图元图像中的像素绘制到渲染目标表面。可以对具有共面多边形的图元使用这一技术,这样可以保证它们能够被正确地渲染。
比如,要把一些轮胎痕迹及黄色警戒线渲染到一条公路上。这些痕迹会直接位于公路的表面上,痕迹的Z值与公路的Z值完全相同。这样,深度缓存就不能清楚地将它们两者分离开来,位于后面的图元上的一些像素就会被渲染到前面的图元上。最终的图像就会在帧与帧之间产生微弱的闪光。这种效果称为“z fighting”或”flimmering”。
要解决这一问题,就要使用一个模板来将贴纸出现的地方的后面图元的像素掩盖掉。然后,关闭z-buffering,将前面力克的图像渲染到目标表面上被掩盖掉的区域上。
也可以使用多纹理融合来解决这一问题,但是这样做就会限制其他使用我纹理整合才能实现的效果的数量。而使用模板缓存就可以节省多纹理融合的能力,从而实现更多的特殊效果。
3) 合成
可以使用模板缓存来将2D或3D图像合成到一个3D场景中。用模板缓存中的一个掩模来封闭渲染目标表面上的某个区域,然后就可以将2D信息,例如文本或位图,填写到被封闭的区域。另一方面,程序还可以在渲染目标表面上被模板掩模掉的区域绘制额外的3D图元,甚至可以绘制一个完整的场景。
游戏中经常将多个3D场景合成在一起。例如,驾驶类游戏中通常要显示一个后视镜,镜中显示了驾驶员背后的3D场景,这时就可以用合成技术来将后视的三维场景合成到后视镜中。
4) 轮廓与剪影
模板缓存还能够用来实现一些抽象的效果,如轮廓与剪影。
如果将一个模板掩模应用到一个图元有相同形状但是尺寸小一些的图像上,那么最终的图像就会得到这个图元的轮廓。然后,可以在模板掩模的区域填充一个具有固定颜色的图像,从而得到一种类似浮雕的效果。
如果模板掩模的大小和形状与图元的大小和形状完全一样,那么最后的图像就会在图元存在的地方出现一个“洞”。可以将这个“洞”填充上黑色,这样就得到了这个图元的剪影效果。
2、 实现模板深度
下面通过一个具体的例子来说明如何在程序中使用模板实现一些特殊的效果。在这个例子中,通过模板缓存来记录一个模型的层次深度,并用不同的颜色表示不同的深度。
void Render()
{
//清空后备缓存和深度缓存
g_pd3dDevice->Clear(0,NULL,
D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(0,0,0),1.0f,0);
g_ppd3dDevice->BeginScene();
SetStateForRecordingDepthComplexity();
SetupMatrices();
g_pMesh.Reder(g_pd3dDevice);
ShowDepthComplexity();
g_pd3dDevice->EndScene();
g_pd3dDevice->Present(NULL,NULL,NULL,NULL);
}
在模板深度效果的这个例子中,按通常的程序框架建立一个程序,并在其中导入一个直升机模型,通过对时间的响应,直升机会在划定的区域做盘旋飞行。这个例子的核心程序部分在Render()函数中完成,下面说明一下函数Render().
先清空所有的缓存区,包括目标缓存区、深度缓存区和模板缓存区,然后开始绘图。通过调用函数SetStateForRecordingDepthComplexity()开始做模板深度的前期准备工作。在SetupMatrices()中完成对时间的响应,并把时间的变化转化为直升机飞行位置的变化。接着调用直升机模型的演示函数。最后通过ShowDepthComplexity()函数与前面的模板设置相对应,完成模板的所有设置。
将这段代码中最重要的两个函数做详细的介绍如下:
SetStateForRecordingDepthComplexity()函数用于在渲染前对模板操作做相应的设置。
实现如下:
SetStateForRecordingDepthComplexity()
{
//清空模板缓存
Device->Clear(0L,NULL,D3DCLEAR_STENCIL,0x0,1.0f,0L);
//执行模板测试,并打开模板功能开关
Device->SetRenderState(D3DRS_TENCILENABLE,TRUE);
//将模板测试函数设为总是成功
Device->SetRenderState(D3DRS_STENCILFUNC,D3DCMP_ALWAYS);
//设置模板参考值为0
Device->SetRenderState(D3DRS_STENCILREF,0);
//模板掩码设为0
Device->SetRenderState(D3DRS_STENCILMASK,0x00000000);
//设置可以写到模板缓存的写入掩码值
Device->SetRendState(D3DRS_STENCILWRITEMASK,0xffffffff);
//深度测试失败对模板缓存中的每个像素做增量操作
Device->SetRenderState(D3DRS_STENCILZFAIL,D3DSTENCILOP_INCRSAT);
//模板测试失败,保持模板缓存中的值
Device->SetRenderState(D3DRS_STENCILFAIL,D3DSTENCILOP_KEEP);
//模板测试成功,对模板缓存中的值做增量操作
Device->SetRenderState(D3DRS_STENCILPASS,D3DSTENCILOP_INCRSAT);
}
ShowDepthComplexity()函数用于模型渲染完后对模板所做的操作,实现如下:
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_STENCILFUNC,D3DCMP_NOTEQUAL);
Device->SetRenderState(D3DRS_STENCILREF,0);
//设置目标缓存区为黑色
Device->Clear(0L,NULL,D3DCLEAR_TARGET,0x00000000,1.0f,0L);
//设置渲染状态用于视口区
//这个区域的颜色将通过入口
Device->SetFVF(D3DFVF_XYZRHW);
Device->SetStreamScource(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);
经过这一系列的设置操作,模型就会按不同的模板深度显示出不同的颜色。重要的是要注意模板设置的步骤和次序,在渲染前设置模板测试的规则,在渲染后根据模板值做相应的操作,以达到要到的效果。