模板测试原理是利用整块模板缓存值Value,模板参数单个元素ref,公用的模板掩码mask,还有模板运算符opr,通过公式: ref & mask opr value & mask,再渲染物体是如果物体像素Mxy对应的后台缓存Bxy上的模板测试为真那么将物体上的该像素写入后台缓存。其中使用模板缓存一般要先设定模板缓存的值( 如果不设定用0防止二次融合也是可以的) 设置模板缓存的值时候可以设置模板写掩码,然后是根据测试结果(或者设置为测试Aways)设置模板缓存的更新方式如果是replace那么设置的是用模板参考值代替模板缓存值,再渲染一个物体例如镜面那么镜面对应的后台缓存上的像素被写入了模板缓存。渲染真正想要通过模板测试的物体时候例如镜面中的茶壶,也需要设置模板缓存的更新方式,模板比较函数,Z值清理避免遮挡剔除,和后台缓存的融合方式,渲染绕序等设置,避免绘制出现异常,模板绘制完后需要重置这些设置,和禁用模板测试。
stencil需要考虑深度剔除(禁用深度缓存),背面剔除( 绕序设置 ),和绘制层次(要使用融合来避免)。
Stencil模板缓存应用性能开销几乎为0(但是一般stencil都需要blend融合开启为了混合颜色),blend融合消耗的性能却是比较大的(有时间测试下),融合深度缓存模板缓存参数配置,绕序设置组合可以得到很多想要的效果,以后要多注意。
一、概念和应用
1.融合和模板测试的区别
2.模板缓存只有一个且模板像素的内存位置和后台缓存的内存位置是一一对应的,可根据前面的物体例如镜子先写入后台缓存,后面绘制的源像素根据前面写入的模板值和设置的参考值,和比较函数,决定是否写入后台缓存。
3. D3D中的镜像是基于任意平面的镜像也就是包含了平移回原点镜像和平移回去,但是由于茶壶的顶点是基于本地坐标系的,所以要先平移到茶壶所在的位置,然后进行基于任意平面的镜像,才会得到正确的镜像后的顶点位置。
4.要绘制出来镜像,需要:
1)清空深度缓存,避免深度剔除。
2) 颜色进行融合防止后面绘制的到前面去了。
3)改变渲染绕序,绘制茶壶背面的三角形顶点,避免背面剔除。
5.模板可以按照模板绘制出来物体,可以按照模板避免再绘制物体; 绘制阴影时候,利用模板可以避免二次融合。
6. 相同的深度缓存会导致深度缓存冲突,最终导致渲染闪烁;绘制阴影中禁用深度缓存可以避免闪烁。
模板缓存是提供一个模板缓存(和深度缓存一起,和后台缓存对应),对渲染到后台缓存的像素进行过滤,标记来自于目标像素,后写入的像素就被过滤到后台缓存渲染。
总之,Blend目标是融合透明度,过滤像素可以通过源像素和像素的透明度;Stencil目标是过滤渲染到后台缓存的像素,过滤像素来自于目标像素和整个后台缓存表面。(源像素是当前操作的像素,目标像素是已经写入后台缓存中的像素)
可用于实现镜面和平面阴影,防止二次融合,阴影体(Shadow Volume),消融(Dissolving)与淡入淡出(fades),深度复杂性的可视化(Visualizing depthcomplexity),轮廊图(Outline)和侧影效果(Silhouette), 几何实体(solid geometry)的构建,
修正共面引起的深度冲突(深度偏置机制depth bias mechanism或绘制了一个深度的物体后禁止深度测试SetRenderState(D3DRS_ZENALE, false);
1.启用模板测试
先检查硬件是否支持模板测试。
D3DCAPS9 caps;
Device->GetDeviceCaps(&caps);
boolbStencilSupport = false;
if(caps.StencilCaps& D3DSTENCILCAPS_INCR/*D3DSTENCILCAPS_TWOSIDED*/!= 0)
{
bStencilSupport = true;
}
// 开启模板测试
Device->SetRenderState(D3DRS_STENCILENABLE, true);
//绘制前需要清空模板缓存为0:
Device->Clear(0, 0,
D3DCLEAR_TARGET |D3DCLEAR_ZBUFFER | D3DCLEAR_STENCIL,
0xff000000, 1.0f, 0L);
2. 指定深度缓存/模板缓存格式
D3DPRESENT_PARAMETERS d3dpp;
d3dpp.EnableAutoDepthStencil = true;
d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8;
在我们创建深度缓存的同时一个模版缓存能够被创建。当指定深度缓存格式的时候,我们同时指定模版缓存的格式。这样,模版缓存和深度缓存分享同一个离屏表面缓存,但是每个像素被指定到各自缓存内存片段中。
下面列出了3种深度/模版缓存的格式:
D3DFMT_D24S8—一个32位深度/模版缓存,其中24位为深度缓存,8位为模版缓存。
D3DFMT_D24X4S4—一个32位深度/模版缓存,其中24位为深度缓存,位为模版缓存,还有4位留着不用。
D3DFMT_D15S1—一个16位深度/模版缓存,其中15位为深度缓存,1位为模版缓存。
3.模板测试算法模型
能够使用模版缓存来阻止渲染后缓存中的某些部分,阻止特殊像素被写是通过模版测试(stencil test)来决定的,这是通过下面的表达式来完成的:
(ref &mask) ComparisonOperation (value & mask)
// value是模板缓存中的值,通常是目标像素写入;对于接下来要写入的源像素(表面)就要取得对应位置的模板缓存值进行模板测试。
模版测试是对每个像素进行的,假设模版是被允许。将有两个操作:
左手边操作数(LHS=ref&mask) 右手边操作数(RHS=value&mask)
模版测试比较LHS和RHS,通过比较运算来指定。全部的运算都得到一个布尔值(true/false)。假如结果是true,那么我们把像素写入后缓存。反之,就阻止像素被写入后缓存。如果像素不能被写入后缓存,那么它也不能被写入深度缓存。
模板测试在固定渲染管道中的位置,在alpha后,在depth测试前:
模板缓存的一次渲染初始化,模板测试过滤二次渲染,二次渲染到目标缓存过程:
4.控制模版测试
指定参考值(stencil reference)和掩码(mask value),以便进行比较运算。虽然不能明确地设定模版值(stencilvalue)但是能够控制写入模版缓存的值。
模版参考值(Reference Value)
模版参考值ref的默认值为0,通过设置D3DRS_STENCILREF渲染状态来改变:
Device->SetRenderState(D3DRS_STENCILREF,0x1);//值常用16位表示
模版掩码(stencil mask)
模版掩码值mask是被用来掩饰(隐藏)在ref和value变量中的位。它的默认值是0xffffffff,也就是没有掩饰任何位,通过设置D3DRS_STENCILMASK渲染状态来改变,例掩饰高16位:
Device->SetRenderState(D3DRS_STENCILMASK,0x0000ffff);
比较运算
通过设置D3DRS_STENCILFUNC渲染状态来设置比较运算。这个比较运算能够被
D3DCMPFUNC的任何成员类型列举:
typedef enum D3DCMPFUNC {
D3DCMP_NEVER = 1,//Always fail the test.
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,//always pass the test
D3DCMP_FORCE_DWORD = 0x7fffffff
}D3DCMPFUNC, *LPD3DCMPFUNC;
模版值(Stencil Value)
在模版缓存中我们进行模版测试的当前像素。对像素进行模版测试,那么该值将被写入该模版缓存。不能明确地设置个别模版值,但是可以清除模版缓存。能够使用模版渲染状态来控制将什么写入模版缓存。
1).更新模板缓存
除了决定是否写或阻止一个特殊像素被写入后缓存以外,我们能够定义模版缓存基于三种可能的案例怎样被更新:
对于[i][j]处像素模版测试失败:
Device->SetRenderState(D3DRS_STENCILFAIL,StencilOperation);
对于[i][j]处像素深度测试失败:
Device->SetRenderState(D3DRS_STENCILZFAIL,StencilOperation);
对于[i][j]处像素模版测试和深度测试都成功:
Device->SetRenderState(D3DRS_STENCILPASS,StencilOperation);
StencilOperation是以下常数
typedef enum D3DSTENCILOP {
D3DSTENCILOP_KEEP = 1,//不改变模版缓存
D3DSTENCILOP_ZERO = 2,//设置模版缓存入口为0。
D3DSTENCILOP_REPLACE = 3,//指定用模版参考值来替换模版缓存入口
D3DSTENCILOP_INCRSAT = 4,//指定增加模版缓存入口。假如增加的值超过了允许的最大值,我们就设置它为最大值。
D3DSTENCILOP_DECRSAT = 5,//指定减少模版缓存入口。假如减少后的值小于了,我们就设置它。
D3DSTENCILOP_INVERT = 6,//指定按位取反模版缓存入口。
D3DSTENCILOP_INCR = 7,//指定增加模版缓存入口。假如增加的值超过了允许的最大值,我们就设置它为0。
D3DSTENCILOP_DECR = 8,//指定减少模版缓存入口。假如减少后的值小于0,我们就设置它为允许的最大值。
D3DSTENCILOP_FORCE_DWORD = 0x7fffffff
}D3DSTENCILOP, *LPD3DSTENCILOP;
2).模版写掩码
除了已经提及的模版渲染状态之外,我们能够设置一个写掩码(write mask)它将掩饰我们写进模版缓存的任何值的位。我们能够通过D3DRS_STENCILWRITEMASK渲染状态来设置写掩码。它的默认值是0xffffffff。下面的例子是掩饰高16位:
Device->SetRenderState(D3DRS_STENCILWRITEMASK,0x0000ffff);
二、应用:
1.阴影的绘制(只有在固定镜面区域显示影子)
对镜面里面物体的绘制,水中的倒影,鱼缸的反射,车外的风景。
影子本质上是把物体按照灯光照射方向平行地投射到平面n*p+d=0
之上。同样的,图8.7中所示的点光源,影子本质上是把物体按照透视画法从光源投射到平面n*p+d=0之上。
我们能够使用一个矩阵来表示从一个顶点p变换到平面n*p=d=0上的s的变化。而且,我们能够用同一个矩阵来表现正交投影和透视投影。
// 反射矩阵
D3DXMATRIX *D3DXMatrixReflect(
__inout D3DXMATRIX *pOut,//输出反射矩阵
__in const D3DXPLANE *pPlane//反射平面
);
// 阴影矩阵
D3DXMATRIX *D3DXMatrixShadow(
__inout D3DXMATRIX *pOut,
__in const D3DXVECTOR4 *pLight,
__in const D3DXPLANE *pPlane
);
2.防止二次融合
几何学上,当我们将一个物体投影到一个平面上时,很可能会有两个或者更多的投影三角形被重叠到一起。若我们就这样渲染,那么有重叠三角形的地方就会被多次混合以至这些地方将会变得更黑。 我们设置模版测试为允许像素第一次被渲染。即,当把影子像素渲染到后缓存时,我们同时在模版缓存中做好标记。然后,如果试图把像素向一个已经渲染过的地方写,那么模版测试将会失败。
例子:
// 镜子和茶壶都写了一遍
void RenderScene() { // draw teapot Device->SetMaterial(&TeapotMtrl); Device->SetTexture(0, 0); D3DXMATRIX W; // 将茶壶平移到这个位置 D3DXMatrixTranslation(&W, TeapotPosition.x, TeapotPosition.y, TeapotPosition.z); Device->SetTransform(D3DTS_WORLD, &W); Teapot->DrawSubset(0); D3DXMATRIX I; D3DXMatrixIdentity(&I); Device->SetTransform(D3DTS_WORLD, &I); Device->SetStreamSource(0, VB, 0, sizeof(Vertex)); Device->SetFVF(Vertex::FVF); // draw the floor Device->SetMaterial(&FloorMtrl); Device->SetTexture(0, FloorTex); Device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2); // draw the walls Device->SetMaterial(&WallMtrl); Device->SetTexture(0, WallTex); Device->DrawPrimitive(D3DPT_TRIANGLELIST, 6, 4); // draw the mirror Device->SetMaterial(&MirrorMtrl); Device->SetTexture(0, MirrorTex); Device->DrawPrimitive(D3DPT_TRIANGLELIST, 18, 2); }
// 然后根据镜子写的模板,来写茶壶的镜像
void RenderMirror() { // // Draw Mirror quad to stencil buffer ONLY. In this way // only the stencil bits that correspond to the mirror will // be on. Therefore, the reflected teapot can only be rendered // where the stencil bits are turned on, and thus on the mirror // only. // D3DCAPS9 caps; Device->GetDeviceCaps(&caps); bool bStencilSupport = false; if(caps.StencilCaps & D3DSTENCILCAPS_INCR/*D3DSTENCILCAPS_TWOSIDED*/ != 0) { bStencilSupport = true; } // 启用模板测试,前面需要绘制的物体都绘制过了包括镜子 Device->SetRenderState(D3DRS_STENCILENABLE, true); // 对于唯一的后台模板缓存(和深度缓存一起),这里设置为全部成功 Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS); // 模板参考值设置为1 Device->SetRenderState(D3DRS_STENCILREF, 0x1); // 模板掩码设置 Device->SetRenderState(D3DRS_STENCILMASK, 0xffffffff); // 模板缓存写掩码设置 Device->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff); // 深度测试失败(只有D3DRS_STENCILZFAIL没有D3DRS_ZFAIL)保留深度值,用于绘制镜面中的茶壶 // State-based .... are performed in the order: Scissor Test(裁剪) , Alpha Test, Stencil Test, and Depth Test(剔除). // 见https://msdn.microsoft.com/en-us/library/windows/desktop/bb205120%28v=vs.85%29.aspx Device->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP); // 模板测试失败保留原来的值,用于绘制镜面中的茶壶 Device->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP); // 模板缓存值,设置为模板参考值 Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE); // disable writes to the depth and back buffers // 禁用写深度缓存,只是用来写模板缓存中的模板值 Device->SetRenderState(D3DRS_ZWRITEENABLE, false); // 用融合来禁止写后台缓存 Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true); Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO); Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE); // draw the mirror to the stencil buffer // 再写一遍镜子,这次只是写入模板缓存值,后台缓存和深度缓存都因为禁止了不写入 Device->SetStreamSource(0, VB, 0, sizeof(Vertex)); Device->SetFVF(Vertex::FVF); Device->SetMaterial(&MirrorMtrl); Device->SetTexture(0, MirrorTex); D3DXMATRIX I; D3DXMatrixIdentity(&I); Device->SetTransform(D3DTS_WORLD, &I); Device->DrawPrimitive(D3DPT_TRIANGLELIST, 18, 2); // re-enable depth writes // 重写启用写深度缓存 Device->SetRenderState( D3DRS_ZWRITEENABLE, true ); // only draw reflected teapot to the pixels where the mirror // was drawn to. // 设置模板比较函数,检测到源像素对应的模板缓存位置中有相等于模板参考值的源像素才写入后台缓存,用于写镜面中的茶壶 Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL); // 模板缓存的更新设置为保持 Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP); // position reflection D3DXMATRIX W, T, R; // 平面为ax + by + cz + dw = 0. n法向量为(a,b,c), dw为平面到原点的距离 D3DXPLANE plane(0.0f, 0.0f, 1.0f, 0.0f); // xy plane // 得到相对于任意平面(此为 xy plane)的镜像的矩阵,要得到任意平面的不一定过原点 // 所以要先平移到原点,然后镜像,再逆向平移回去才会得到正确结果 D3DXMatrixReflect(&R, &plane); // 平移到茶壶所在的坐标系,因为茶壶用本地坐标系描述 D3DXMatrixTranslation(&T, TeapotPosition.x, TeapotPosition.y, TeapotPosition.z); // 物体是在本地坐标系中,但是要在世界坐标系中绘制物体,所以变换矩阵要先平移到茶壶所在的坐标系,然后镜像 // 本地坐标系中的茶壶顶点,乘以W矩阵就得到镜像中的顶点位置了 W = T * R; // clear depth buffer and blend the reflected teapot with the mirror // 1.开始绘制茶壶镜像,清空下深度缓存目的是绘制比镜子深的茶壶的镜像 Device->Clear(0, 0, D3DCLEAR_ZBUFFER, 0, 1.0f, 0); // 2.用融合防止,茶壶绘制到了镜子前面,让后台缓存的镜子颜色和茶壶颜色进行融合 Device->SetRenderState( D3DRS_ZWRITEENABLE, true ); Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR); Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO); // Finally, draw the reflected teapot // 设置变换,对将要绘制的顶点产生影响 Device->SetTransform(D3DTS_WORLD, &W); Device->SetMaterial(&TeapotMtrl); Device->SetTexture(0, 0); // 3.因为茶壶镜像属于茶壶的背面,所以为了防止背面剔除,设置过滤掉顺时针的三角形像素 Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // 绘制出来 Teapot->DrawSubset(0); // Restore render states. // 关闭融合,这个是为了防止写和写后台缓存的 Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false); // 关闭模板测试 Device->SetRenderState( D3DRS_STENCILENABLE, false); // 设置绕序回来,背面剔除 Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); }
阴影绘制中的防止二次融合,和禁用深度缓存避免屏幕闪烁:
void RenderShadow() { // 开启模板写,可以写模板和后台缓存,为了防止二次融合 Device->SetRenderState(D3DRS_STENCILENABLE, true); Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL); Device->SetRenderState(D3DRS_STENCILREF, 0x0); Device->SetRenderState(D3DRS_STENCILMASK, 0xffffffff); Device->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff); // 深度检测失败时候,保留深度值 Device->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP); // 模板检测失败,保留值 Device->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP); // 模板检测设置为增长 Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_INCR); // increment to 1 // position shadow D3DXVECTOR4 lightDirection(0.707f, -0.707f, 0.707f, 0.0f); D3DXPLANE groundPlane(0.0f, -1.0f, 0.0f, 0.0f); D3DXMATRIX S; D3DXMatrixShadow( &S, &lightDirection, &groundPlane); D3DXMATRIX T; D3DXMatrixTranslation( &T, TeapotPosition.x, TeapotPosition.y, TeapotPosition.z); D3DXMATRIX W = T * S; Device->SetTransform(D3DTS_WORLD, &W); // alpha blend the shadow // 开启融合 Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true); // 融合因子取决于alpha值 DWORD dwArg1Value = 0; Device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &dwArg1Value); DWORD dwOpValue = 0; Device->GetTextureStageState(0, D3DTSS_ALPHAOP, &dwOpValue); // 默认是alpha来自于D3DTA_TEXTURE; D3DTSS_ALPHAOP来自于D3DTSS_COLORARG1; Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); // 设置融合光照的材质 D3DMATERIAL9 mtrl = d3d::InitMtrl(d3d::BLACK, d3d::BLACK, d3d::BLACK, d3d::BLACK, 0.0f); mtrl.Diffuse.a = 0.5f; // 50% transparency. // Disable depth buffer so that z-fighting doesn't occur when we // render the shadow on top of the floor. // 禁用深度缓存,防止z值一样的像素绘制,出现闪烁 Device->SetRenderState(D3DRS_ZENABLE, false); // 通过stencil测试的会绘制到后台缓存上,因为stencil测试是等于0时才绘制,防止二次融合 Device->SetMaterial(&mtrl); Device->SetTexture(0, 0); Teapot->DrawSubset(0); // 启用深度缓存,后面不需要避免闪烁了 Device->SetRenderState(D3DRS_ZENABLE, true); // 关闭融合,提高性能 Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false); // 关闭stencil Device->SetRenderState(D3DRS_STENCILENABLE, false); }