学习目标:
- 理解模板缓存的工作原理,如何创建模板缓存以及如何对模板缓存进行控制
- 了解如何实现镜面效果,以及如何使用模板缓存阻止物体映像在非镜面区域中的绘制
- 掌握如何绘制阴影,以及如何借助模板缓存阻止“二次融合”
什么是模板?:
模板缓存是一个用于获得某种特效的离屏缓存,模板缓存的分辨率与后台缓存和深度缓存的分辨率完全相同,所以模板缓存中的像素与后台缓存和深度缓存中的像素是一一对应的,它允许我们动态的,有针对性的决定是否将某个像素写入后台缓存中。
模板缓存的使用:
为了使用模板缓存,在 Direct3D 初始化时,我们首先必须查询当前设备是否支持模板缓存;如果设备支持模板缓存,我们还必须将其启用。启用模板缓存时必须将绘制状态 D3DRS_STENCILENABLE 设为 true;若要禁用模板缓存,则该状态设为 false,下列代码演示如何启用和禁止模板缓存:
启用模板缓存:
//启用模板缓存
device->SetRenderState(D3DRS_STENCILENABLE, true);
//禁用模板缓存
device->SetRenderState(D3DRS_STENCILENABLE, false);
注意: DirectX 9.0中增加来双面模板的特性,该功能可通过消减绘制阴影体所需的绘制路径,从而提升阴影体的绘制速度,这里未使用,详情参考 SDK 文档。
我们可以使用 IDirect3DDevice9::Clear 方法将模板缓存清空为一个默认值,该方法也可对后台缓存和深度缓存进行清空操作。
清空模板缓存:
//清空后台缓存|深度缓存|模板缓存
device->Clear(0, 0, D3DCLEAR_TARGET|D3DCLEAR_ABUFFER|D3DCLEAR_STENCIL, 0xff000000, 1.0f, 0);
注意在第3个参数中增加的 D3DCLEAR_STENCIL 表面我们要对模板缓存进行清空操作,第6个参数用于指定要将模板缓存清空为何值,在上面的例子中,将该值设为0。
注意:根据某报告,在现代硬件中使用模板缓存可被认为是 “没有计算开销的运算”,当然前提是已经在使用深度缓存了。
模板缓存的格式查询:
模板缓存可与深度缓存一同创建,为深度缓存指定格式时,我们可以同时指定模板缓存的格式。实际上,模板缓存和深度缓存共享同一个离屏的表面缓存,而每个像素的内存段被划分为若干部分,分别与某种特定缓存相对应,例如,考虑如下3种深度/模板缓存格式:
- D3DFMT_D24S8 该格式的含义是,已创建一个32位的深度/模板缓存,其中每个像素的 24 位指定给深度缓存,8位指定给模板缓存。
- D3DFMT_D24X4S4 该格式的含义是,已创建一个32位的深度/模板缓存,其中每个像素的 24 位指定给深度缓存,4 位指定给模板缓存,其余4位不用。
- D3DFMT_D15S1 该格式的含义是,已创建一个16位的深度/模板缓存,其中每个像素的 15 位指定给深度缓存,1 位指定给模板缓存。
注意,一些格式没有模板缓存分配任何空间。例如 D3DFMT_D32 格式仅创建一个 32 位的深度缓存。各种图像卡对模板缓存的支持情况各异,例如某些卡可能不支持 8 位模板缓存。
模板测试:
如前所述,可用模板缓存来阻止对应后台缓存中某些特定区域进行绘制,判定是否将某个像素写入后台缓存的决策过程称为模板测试,其表达式如下:
(ref & mask) ComparisonOperation (Value & mask)
假定模板已处于启用状态,则每个像素都需进行模板测试。模板测试需要如下两个操作数
- 左操作数(LHS = ref & mask) 该值由应用程序定义的模板参考值 (ref) 和模板掩码 (mask) 通过按位与运算得到。
- 右操作数(RHS = value & mask) 该值由当前进行测试的像素的模板缓存中的数值 (value) 与模板掩码 (mask) 通过按位与运算得到。
模板测试的下一步内容是依据所指定的比较规则对 LHS 和 RHS 进行比较,上诉表达式的运算结果为布尔类型(true 或 false),如果测试结果为 true,便将该像素写入后台缓存,如果测试结果为 false,我们将阻止该像素被写入后台缓存。当然,一个像素不被写入后台缓存时,也不会被写入深度缓存。
模板测试的控制:
为了增加灵活性, Direct3D 允许我们在模板测试中对上诉变量进行控制,所以,我们可以对模板参考值,模板掩码,以及对比较函数进行设定。虽然我们无法显示设定模板值,但我们确实可以控制那些值可被写入模板缓存(除模板缓存的清楚操作外)。
- 模板参考值:
模板参考值 (ref) 的默认值为 0,但我们可用 D3DRS_STENCILREF 绘制状态改变该值,例如下列代码将模板参考值设为 1:
device->SetRenderState(D3DRS_STENCILREF, 0x1);
注意,倾向于使用16进制数可使整数的位排列一目了然,而且进行按位逻辑运算时也比较容易看出结果。
- 模板掩码:
模板掩码 (mask) 用于屏蔽 ref 和 value 变量中的某些位,其默认值为 0xffffffff,表示不屏蔽任何位,我们可以借助绘制状态 D3DRS_STENCILMASK 来修改该掩码值,下面的例子屏蔽来高 16 位:
device->SetRenderState(D3DRS_STENCILMASK, 0x0000ffff);
注意,如果对于位和屏蔽有些困惑,建议复习有关二进制,十六进制以及位运算的相关知识
- 模板值:
如前所述,该值是当前待测试像素在模板缓存中的对应值,例如我们正在对第 i 行,第 j 列的像素进行测试,则 value 就是模板缓存中第 i 行 j 列的值,我们不能显示的单独设置模板值,但可以对模板缓存进行清空操作,此外,我们还可以用模板的绘制状态控制将要写入模板缓存的内容。 - 比较运算:
可通过绘制状态 D3DRS_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, /* force 32-bit size enum */
} D3DCMPFUNC, *LPD3DCMPFUNC;
- D3DCMP_NEVER 模板测试总是失败,即比较函数总是返回 false
- D3DCMP_LESS 若 LHS < RHS,则模板测试成功
- D3DCMP_EQUAL 若 LHS = RHS,则模板测试成功
- D3DCMP_LESSEQUAL 若 LHS<= RHS,则模板测试成功
- D3DCMP_GREATER 若 LHS > RHS,则模板测试成功
- D3DCMP_NOTEQUAL 若 LHS!= RHS,则模板测试成功
- D3DCMP_GREATEREQUAL 若 LHS >= RSH,则模板测试成功
- D3DCMP_ALWAYS 模板测试总是成功,即比较函数总是返回true
模板缓存的更新:
除了决定一个具体像素是否应被写入后台缓存,我们还可以基于以下3种可能的情形定义模板缓存中的值如何进行更新
- 第 i 行,第 j 列的像素模板测试失败,这种情况下,我们可借助绘制状态 D3DRS_STENCILFAIL 将模板缓存中处于同样位置的项的更新方式定义如下:
device->SetRenderState(D3DRS_STENCILFAIL, StencilOperation);
- 第 i 行,第 j 列的像素深度测试失败,这种情况下,我们可借助绘制状态 D3DRS_STENCILFAIL 将模板缓存中处于同样位置的项的更新方式定义如下:
device->SetRenderState(D3DRS_STENCILZFAIL, StencilOperation);
- 第 i 行,第 j 列的像素深度测试和模板测试均成功,此种情况下,我们可借助绘制状态 D3DRS_STENCILPASS 将模板缓存中处于同样位置的项的更新方式定义如下:
device->SetRenderState(D3DRS_STENCILPASS, StencilOperation);
其中 StencilOperation 可取以下预定义常量:
- D3DSTENCILOP_KEEP 不更新模板缓存中的值,即保留当前值
- D3DSTENCILOP_ZERO 将模板缓存中的值设为 0
- D3DSTENCILOP_REPLACE 用模板参考值替代模板缓存中的对应值
- D3DSTENCILOP_INCRSAT 增加模板缓存中的对应数值,如果超过最大值,取最大值
- D3DSTENCILOP_DECRSAT 减小模板缓存中的对应数值,如果超过最小值,取最小值
- D3DSTENCILOP_INVERT 模板缓存中的对应值按位取反
- D3DSTENCILOP_INCR 增加模板缓存中的对应数值,如果超过最大值,则取0
- D3DSTENCILOP_DECR 减小模板缓存中的对应数值,如果小于0,则取最大值
模板写掩码:
除来前面的绘制状态,我们还可以设置写掩码 (write mask),该值可屏蔽我们将写入模板缓存的任何值的某些位,可用绘制状态 D3DRS_STENCILWRITEMASK 来设定写掩码的值,其默认值为 0xffffffff,下列中对高 16 位实施来屏蔽:
device->SetRenderState(D3DRS_STENCILWRITEMASK, 0x0000ffff);
上面部分是模板使用方法,接下来是如何生成镜面效果
镜面效果
要想在程序中实现镜面效果,需要解决两个问题,第一,为了能够正确的绘制物体在镜面中的印象,我们必须了解对于任意平面物体如何成像。第二,必须将某一表面区域标记为镜像,接下来就可以只绘制处于指定镜面区域中那部分物体的印象了。
第一个问题借助向量几何解决,第二个问题借助模板缓存来解决,接下来讨论两个问题的解决方案,最后将二者综合起来实现一个例子。
成像中的数学问题:
镜像,也叫做反射,是一种变换,其作用是将物体沿直线(2D中)或平面(3D中)翻折。