第十章 模板渲染

模板缓冲区是我们可以用来实现一些特殊效果的屏幕外缓冲区。模板缓冲区与后台缓冲区和深度缓冲区具有相同的分辨率,使得模板缓冲区中的第ij个像素与后台缓冲区和深度缓冲区中的第ij个像素相对应。从§4.1.5中回想一下,当一个模板缓冲区被指定时,它被附加到深度缓冲区。顾名思义,模板缓冲区作为一个模板,并允许我们阻止某些像素片段渲染到后台缓冲区。

例如实现镜子时,我们需要在镜子的平面上反射一个物体;但是,我们只想把镜子区域反射回去。使用模板缓冲区,除非它在镜子区域中,否则会被阻止反射的渲染,(见图10.1)。

模板缓冲区(以及深度缓冲区)通过ID3D11DepthStencilState接口进行控制。像混合一样,界面提供了一组灵活而强大的功能。通过学习现有的示例应用程序,学习如何有效地使用模板缓冲区。一旦了解了模板缓冲区的用法,您将更好地了解如何将其用于您自己的特定需求。

10-1
图10.1 (左)反射的头骨在镜子中正确显示。 反射没有通过墙砖显示,因为它没有通过这个区域的深度测试。 然而,看着墙后,我们能够看到倒影,从而打破了错觉(反射只能通过镜子显现出来)。 (右)通过使用模板缓冲区,我们可以阻止反射的头骨被渲染,除非它被绘制在镜子中。

学习目标:
1.了解如何使用 ID3D11DepthStencilState接口控制深度和模板缓冲区设置。
2.要学习如何使用模板缓冲区来实现镜像,以防止反射被绘制到非镜面上。
3.识别双重混合并理解模板缓冲区如何防止它。
4.解释深度复杂性并描述两种方式可以测量场景的深度复杂度。

10.1深度/标准格式和清除

可以将 深度/模板缓冲区理解为一个纹理,它必须用特定的数据格式创建。用于深度/模板缓冲的格式如下所示:
1.DXGI_FORMAT_D32_FLOAT_S8X24_UINT:指定32位浮点深度缓冲区,其中8位(无符号整数)保留用于映射到[0,255]范围的模板缓冲区,24位不用于填充。
2.DXGI_FORMAT_D24_UNORM_S8_UINT:指定映射到[0,1]范围的无符号24位深度缓冲区,为映射到[0,255]范围的模板缓冲区保留8位(无符号整数)。在我们的D3DApp框架中, 我们创建深度缓冲区,我们指定:
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;

此外,模板缓冲区应该在每帧开始时重置为某个值。 这是用下面的方法(它也清除深度缓冲区)完成的:

void ID3D11DeviceContext::ClearDepthStencilView(
    ID3D11DepthStencilView *pDepthStencilView,
    UINT ClearFlags, FLOAT Depth, UINT8 Stencil);

1.pDepthStencilView:指向我们要清除的深度/模板缓冲区视图的指针。
2.ClearFlags:指定D3D11_CLEAR_DEPTH只清除深度缓冲区; 指定D3D11_CLEAR_STENCIL只清除模板缓冲区; 指定D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL清除两者。
3.Depth:将深度缓冲区中的每个像素设置为的浮点值;它必须是一个浮点数x,使得0≤x≤1。
4.Stencil:设置模板缓冲区的每个像素的整数值;它必须是一个整数n,使得0≤n≤255。

我们已经在我们的演示中每帧都调用了这个方法。 例如:

void MirrorApp::DrawScene()
{
md3dImmediateContext->ClearRenderTargetView(
    mRenderTargetView,
    reinterpret_cast<const float*>(&Colors::Black));

md3dImmediateContext->ClearDepthStencilView(
    mDepthStencilView,
    D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
}

10.2模板测试

如前所述,我们可以使用模板缓冲区来阻止渲染到后台缓冲区的某些区域。阻止特定像素被写入的决定由模板测试决定,其由以下给出:

if(StencilRef & StencilReadMask ⊴ Value & StencilReadMask)
    accept pixel
else
    reject pixel

模板测试是在像素被光栅化(即在输出合并阶段)的情况下进行的,假设模板被启用,并且需要两个操作数:
1.通过将应用程序定义的模板参考值(StencilRef)与应用程序定义的掩蔽值(StencilReadMask)进行“与”操作来确定左侧(LHS)操作数。
2.通过将已测试的特定像素(值)的模板缓冲区中的条目与应用程序定义的遮罩值(StencilReadMask)进行AND运算,确定右侧(RHS)操作数。

请注意,StencilReadMask对于LHS和RHS是相同的。然后,模板测试将LHS与RHS进行比较,如应用程序选择的比较函数⊴所指定的,该函数返回true或false值。如果测试评估为true,则将像素写入后台缓冲区(假设深度测试也通过) 。如果测试结果为false,那么我们阻止像素被写入后台缓冲区。当然,如果一个像素由于模板测试失败而被拒绝,它也不会被写入深度缓冲区。

⊴运算符是D3D11_COMPARISON_FUNC枚举类型中定义的任何一个函数:

typedef enum D3D11_COMPARISON_FUNC
{
    D3D11_COMPARISON_NEVER = 1,
    D3D11_COMPARISON_LESS = 2,
    D3D11_COMPARISON_EQUAL = 3,
    D3D11_COMPARISON_LESS_EQUAL = 4,
    D3D11_COMPARISON_GREATER = 5,
    D3D11_COMPARISON_NOT_EQUAL = 6,
    D3D11_COMPARISON_GREATER_EQUAL = 7,
    D3D11_COMPARISON_ALWAYS = 8,
} D3D11_COMPARISON_FUNC;

1.D3D11_COMPARISON_NEVER:函数总是返回false。
2.D3D11_COMPARISON_LESS:用<运算符替换⊴。
3.D3D11_COMPARISON_EQUAL:用==运算符替换⊴。
4.D3D11_COMPARISON_LESS_EQUAL:用operator运算符替换⊴。
5.D3D11_COMPARISON_GREATER:用>运算符替换⊴。
6.D3D11_COMPARISON_NOT_EQUAL:用!替换⊴! =运算符。
7.D3D11_COMPARISON_GREATER_EQUAL:用≥操作符替换⊴。
8.D3D11_COMPARISON_ALWAYS:函数总是返回true。

10.3深度/标准状态块

创建一个ID3D11DepthStencilState接口的第一步是填写一个D3D11_DEPTH_STENCIL_DESC实例:

typedef struct D3D11_DEPTH_STENCIL_DESC {
    BOOL DepthEnable; // Default True

    // Default: D3D11_DEPTH_WRITE_MASK_ALL
    D3D11_DEPTH_WRITE_MASK DepthWriteMask;

    // Default: D3D11_COMPARISON_LESS
    D3D11_COMPARISON_FUNC DepthFunc;

    BOOL StencilEnable; // Default: False
    UINT8 StencilReadMask; // Default: 0xff
    UINT8 StencilWriteMask; // Default: 0xff
    D3D11_DEPTH_STENCILOP_DESC FrontFace;
    D3D11_DEPTH_STENCILOP_DESC BackFace;
} D3D11_DEPTH_STENCIL_DESC;

10.3.1深度设置

1.DepthEnable:指定true以启用深度缓冲;指定false来禁用它。
当深度测试被禁用时,绘制顺序很重要,即使在一个遮挡物体后面也会绘制一个像素片段(见§4.1.5)。如果禁用深度缓冲,深度缓冲区中的元素也不会更新,无论DepthWriteMask如何设置。
2.DepthWriteMask:可以是D3D11_DEPTH_WRITE_MASK_ZEROD3D11_DEPTH_WRITE_MASK_ALL,但不能同时使用。假设DepthEnable设置为true,D3D11_DEPTH_WRITE_MASK_ZERO将禁止写入深度缓冲区,但深度测试仍将发生。D3D11_DEPTH_WRITE_MASK_ALL启用写入深度缓冲区;如果深度和模板测试都通过,将会写入新的深度。控制深度读取和写入的能力对于实现某些特殊效果是必要的。
3.DepthFunc:指定D3D11_COMPARISON_FUNC枚举类型的成员之一来定义深度测试比较函数。通常这总是D3D11_COMPARISON_LESS,以便进行通常的深度测试,如§4.1.5中所述。也就是说,像素片段只要其深度值小于写入后缓冲区的前一个像素的深度,就可以接受。但是,如您所见,Direct3D允许您在必要时自定义深度测试。

10.3.2模板设置

1.StencilEnable:指定true以启用模板测试; 指定false来禁用它。
2.StencilReadMask:模板测试中使用的StencilReadMask

if(StencilRef & StencilReadMask ⊴ Value & StencilReadMask)
    accept pixel
else
    reject pixel

我们指定不区分大小写,所以,例如,INCR等同于Incr。

10.4 实施平面镜

自然界中的许多表面都是镜面,让我们看到物体的反射。本节介绍如何为3D应用程序模拟镜面。请注意,为了简便,我们只在平面镜上进行讲解。虽然一辆闪亮的汽车可以显示反射; 但汽车的车身是光滑的,圆的,而不是平面的。因此,我们会在类似闪亮的大理石地板上,或者挂在墙上的镜子上渲染反射,换句话说,就是在平面上进行。

以编程方式实现镜像需要我们解决两个问题。 首先,我们必须学会如何反映一个任意的平面的对象,以便我们能够正确地绘制反射。 其次,我们只能在镜子中展示反射,也就是说,我们必须以某种方式将一个表面“标记”为一面镜子,然后在渲染时只绘制反射的物体。 请参考图10.1,它首先介绍了这个概念。

第一个问题很容易用一些解析几何来解决,并在附录C中讨论。第二个问题可以用模板缓冲来解决。

10.4.1镜像概述

当我们绘制反射时,我们也需要反射镜面上的光源。 否则,反射中的照明将不准确。

图10.2显示了绘制物体的反射,我们只需要在镜面上反射它。但是,这引入了图10.1所示的问题。也就是说,物体(这种情况下的头骨)的反射只是我们场景中的另一个物体,如果没有物体遮挡它,那么眼睛就会看到它。但是,反射只能通过镜子来看。我们可以使用模板缓冲区来解决这个问题,因为模板缓冲区允许我们阻止渲染到后台缓冲区的某些区域。因此,如果模板缓冲区没有被渲染到镜像中,我们可以使用模板缓冲区来阻止渲染反射的头骨。 以下概述了完成此步骤的步骤:

10-2
图10.2 眼睛通过镜子看到盒子的反射。为了模拟这一点,我们在镜像平面上反射盒子并像往常一样渲染反射盒子。

10-3
图10.3 将后台缓冲区和模板缓冲区的地板,墙壁和头骨清除为0(用浅灰色表示)。在模板缓冲区上绘制的黑色轮廓线说明了后端缓冲区像素和模板缓冲区像素之间的关系 - 它们不表示模板缓冲区上绘制的任何数据。

1.像平常一样将地板,墙壁和头骨渲染到后台缓冲区(但不是镜像)。 请注意,此步骤不会修改模板缓冲区。
2.将模板缓冲区清除为0.图10.3显示了此时的后台缓冲区和模板缓冲区(我们用一个方框替代了头骨以使绘图更简单)。
3.仅将镜像渲染到模板缓冲区。 我们可以通过创建设置的混合状态来禁用对后台缓冲区的颜色写入

D3D11_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask = 0;

我们可以通过设置禁止写入深度缓冲区

D3D11_DEPTH_STENCIL_DESC::DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;

将镜像渲染到模板缓冲区时,我们将模板测试设置为始终成功(D3D11_COMPARISON_ALWAYS),并指定如果测试通过,则应使用1(StencilRef)替换模板缓冲区条目(D3D11_STENCIL_OP_REPLACE)。 如果深度测试失败,我们指定D3D11_STENCIL_OP_KEEP,这样如果深度测试失败(例如,如果头骨遮住镜像的一部分),模板缓冲区不会发生更改。 因为我们只将镜像渲染到模板缓冲区,所以模板缓冲区中的所有像素将为0,除了与镜像的可见部分对应的像素 - 它们将具有1.图10.4显示了更新 模板缓冲区。 本质上,我们在模板缓冲区中标记镜像的可见像素。

绘制完头骨后,将镜子绘制到模板缓冲区非常重要,以便镜头的镜子像素不能进行深度测试,因此不会修改模板缓冲区。 我们不想打开被遮挡的部分模板缓冲区; 否则反射将通过头骨显示。

10-4
图10.4 将镜像渲染到模板缓冲区,基本上标记模板缓冲区中与镜像的可见部分相对应的像素。模板缓冲区上的纯黑色区域表示模板条目设置为1.请注意,由模块缓冲的模板缓冲区上的区域未被设置为1,因为它未能进行深度测试(该框位于该部分的前面 镜子)。

4.现在我们将反射的头骨渲染到后台缓冲区和模板缓冲区。但回想一下,如果模板测试通过,我们只会渲染到后台缓冲区。这一次,我们将模板测试设置为仅在模板缓冲区中的值等于1时才成功;这是通过使用1的StencilRef和模板操作符D3D11_COMPARISON_EQUAL完成的。通过这种方式,反射的头骨只会渲染到相应的模板缓冲区条目中具有1的区域。由于模板缓冲区中对应于镜像可见部分的区域是唯一具有1的条目,因此反射的头骨只会渲染到镜像的可见部分。
5.最后,我们像往常一样将镜像渲染到后台缓冲区。但是,为了让头骨反射透过(位于镜子后面),我们需要使用透明度混合渲染镜子。如果我们没有渲染具有透明度的镜子,镜子会简单地遮挡反射,因为它的深度小于反射的深度。为了实现这一点,我们只需要为镜像定义一个新的材质实例;我们将漫反射分量的alpha通道设置为0.5,以使反射镜50%不透明,并且如上一章(§9.5.4)中所述,我们使用透明度混合状态渲染反射镜。

mMirrorMat.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
mMirrorMat.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 0.5f);
mMirrorMat.Specular = XMFLOAT4(0.4f, 0.4f, 0.4f, 16.0f);

这些设置提供以下混合等式:

C=0.5Csrc+0.5Cdst C = 0.5 · C s r c + 0.5 · C d s t

假设我们已将反射的头骨像素放置到后台缓冲区,我们看到50%的颜色来自镜像(源),50%的颜色来自头骨(目标)。

10.4.2 定义镜像深度/模板状态

为了实现前面描述的算法,我们需要两个深度/模板状态。 第一个用于绘制镜像以标记模板缓冲区上的镜像像素。 第二个用于绘制反射的头骨,以便它仅被绘制到镜子的可见部分。

//
// MarkMirrorDSS
//
D3D11_DEPTH_STENCIL_DESC mirrorDesc;
mirrorDesc.DepthEnable = true;
mirrorDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
mirrorDesc.DepthFunc = D3D11_COMPARISON_LESS;
mirrorDesc.StencilEnable = true;
mirrorDesc.StencilReadMask = 0xff;
mirrorDesc.StencilWriteMask = 0xff;
mirrorDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
mirrorDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
// We are not rendering backfacing polygons, so these settings do not matter.
mirrorDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
mirrorDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
ID3D11DepthStencilState* MarkMirrorDSS;
HR(device->CreateDepthStencilState(&mirrorDesc, &MarkMirrorDSS));
//
// DrawReflectionDSS
//
D3D11_DEPTH_STENCIL_DESC drawReflectionDesc;
drawReflectionDesc.DepthEnable = true;
drawReflectionDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
drawReflectionDesc.DepthFunc = D3D11_COMPARISON_LESS;
drawReflectionDesc.StencilEnable = true;
drawReflectionDesc.StencilReadMask = 0xff;
drawReflectionDesc.StencilWriteMask = 0xff;
drawReflectionDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// We are not rendering backfacing polygons, so these settings do not matter.
drawReflectionDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;
ID3D11DepthStencilState* DrawReflectionDSS;
HR(device->CreateDepthStencilState(&drawReflectionDesc, &DrawReflectionDSS));

10.4.3 绘制场景

以下代码概述了我们的绘制方法。为了简洁和清晰,我们省略了不相关的细节,例如设置常量缓冲区值(请参阅示例代码了解完整细节)。

//
// Draw the floor, walls and skull to the back buffer as normal.
//…
//
// Draw the mirror to the stencil buffer.
// Here we set the stencil value of visible pixels of the mirror
// to 1, thereby marking “mirror pixels.”
//
// Note that we have to draw the mirror last because we need to
// render the skull into the depth buffer first so that when we
// render the mirror, portions of the mirror that are occluded
// by the skull fail the depth test and do not get rendered
// into the stencil buffer. We do not want to set pixels on
// the stencil buffer that are occluded. Otherwise the reflection
// will show through the skull, too.
//
// Do not write to render target.
md3dImmediateContext->OMSetBlendState(
RenderStates::NoRenderTargetWritesBS, blendFactor, 0xffffffff);
// Render visible mirror pixels to stencil buffer.
// Do not write mirror depth to depth buffer at this point, otherwise
// it will occlude the reflection.
md3dImmediateContext->OMSetDepthStencilState(
RenderStates::MarkMirrorDSS, 1);
pass->Apply(0, md3dImmediateContext);
// Draw mirror.
md3dImmediateContext->Draw(6, 24);
//
// Draw the reflected skull.
//
// Build reflection matrix to reflect the skull.
XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
XMMATRIX R = XMMatrixReflect(mirrorPlane);
XMMATRIX world = XMLoadFloat4x4(&mSkullWorld) * R;
…/
/ Reflect the light source as well.
// Cache the old light directions, and reflect the light directions.
XMFLOAT3 oldLightDirections[3];
for(int i = 0; i < 3; ++i)
{
oldLightDirections[i] = mDirLights[i].Direction;
XMVECTOR lightDir = XMLoadFloat3(&mDirLights[i].Direction);
XMVECTOR reflectedLightDir = XMVector3TransformNormal(lightDir, R);
XMStoreFloat3(&mDirLights[i].Direction, reflectedLightDir);
}
Effects::BasicFX->SetDirLights(mDirLights);
// Reflection changes winding order, so cull clockwise
// triangles instead (see §10.4.4).
md3dImmediateContext->RSSetState(RenderStates::CullClockwiseRS);
// Only render reflection to pixels with a stencil value equal to 1. Only
// visible mirror pixels have a stencil value of 1, hence the skull
// will only be rendered into the mirror.
md3dImmediateContext->OMSetDepthStencilState(
RenderStates::DrawReflectionDSS, 1);
pass->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mSkullIndexCount, 0, 0);
// Restore default states.
md3dImmediateContext->RSSetState(0);
md3dImmediateContext->OMSetDepthStencilState(0, 0);
// Restore light directions.
for(int i = 0; i < 3; ++i)
{
mDirLights[i].Direction = oldLightDirections[i];
}
Effects::BasicFX->SetDirLights(mDirLights);
//
// Draw the mirror to the back buffer as usual but with transparency
// blending so the reflection shows through.
//
// Mirror
md3dImmediateContext->OMSetBlendState(
RenderStates::TransparentBS, blendFactor, 0xffffffff);
pass->Apply(0, md3dImmediateContext);
md3dImmediateContext->Draw(6, 24);

10.4.4 扭曲顺序和反射

当三角形在平面上反射时,其绕线顺序不会反转,因此其面部法线不会反转。因此,面向外部的法线在反射之后变成面向法线的内部法线(见图10.5)。为了解决这个问题,我们告诉Direct3D将逆时针顺序的三角形解释为正面,将三角形顺时针缠绕顺序解释为背面(这与我们通常的惯例相反 - §5.10.2)。这有效地反映了正常的方向,使它们在反射后面向外。 我们通过设置以下光栅器状态来颠倒卷绕顺序约定:

10-5
图10.5 多边形法线不会与反射相反,这使得它们在反射后面向内。

// Note: Define such that we still cull backfaces by making front faces CCW.
// If we did not cull backfaces, then we have to worry about the BackFace
// property in the D3D11_DEPTH_STENCIL_DESC.
D3D11_RASTERIZER_DESC cullClockwiseDesc;
ZeroMemory(&cullClockwiseDesc, sizeof(D3D11_RASTERIZER_DESC));
cullClockwiseDesc.FillMode = D3D11_FILL_SOLID;
cullClockwiseDesc.CullMode = D3D11_CULL_BACK;
cullClockwiseDesc.FrontCounterClockwise = true;
cullClockwiseDesc.DepthClipEnable = true;
ID3D11RasterizerState* CullClockwiseRS;
HR(device->CreateRasterizerState(&cullClockwiseDesc, &CullClockwiseRS));

10.5 实施平面阴影

阴影有助于我们理解场景中的光线在哪里发出,并最终使场景更真实。 在本节中,我们将展示如何实现平面阴影; 即位于平面上的阴影(见图10.6)。

10-6
图10.6 主光源在“镜像”演示中投射平面阴影。

为了实现平面阴影,我们必须首先找到物体投射到平面上的阴影并对其进行几何建模,以便我们可以对其进行渲染。这可以通过一些3D数学来轻松完成。然后,我们用50%透明度的黑色材质渲染描述阴影的三角形。像这样渲染阴影可以引入一些称为“双重混合”的渲染构件,我们在几节中解释它; 我们利用模板缓冲区来防止发生双重混合。

10.5.1 平行光阴影

图10.7显示了一个物体相对于平行光源投射的阴影。 给定方向为L的平行光源,穿过顶点P的光线由 rt=p+tL r ( t ) = p + t L 给出。光线r(t)与阴影平面(n,d)的交点给出s(读者可以在附录C中阅读更多关于光线和平面的信息)。通过拍摄每一条光线 物体与平面的顶点定义了投影的阴影几何。 对于顶点p,其阴影投影由下式给出

s=r(ts)=pnp+dnLL s = r ( t s ) = p − n · p + d n · L L

射线/平面相交测试的细节在附录C中给出。

10-7
图10.7 相对于平行光源投射的阴影。

公式10.1可以写成矩阵
s=[pxpypz1]nLLxnxLxnyLxnzLxdLynynLLynyLynzLydLznzLznynLLznzLzd000nL s ′ = [ p x p y p z 1 ] [ n · L − L x n x − L y n y − L z n z 0 − L x n y n · L − L y n y − L z n y 0 − L x n z − L y n z n · L − L z n z 0 − L x d − L y d − L z d n · L ]

我们称前面的4×4矩阵为方向阴影矩阵,并由Sdir表示。 要看这个矩阵如何等于公式10.1,我们只需要执行乘法。 然而,首先观察到这个方程式修改w分量,使得sw = n·L。因此,当透视分割(§5.6.3.4)发生时,s的每个坐标将被除以n·L; 这就是我们如何使用矩阵在方程10.1中得到n·L的除法。 现在进行矩阵乘法以获得i∈{1,2,3}的第i个坐标si’,然后获得透视分割:
si=(nL)piLinxpxLinypyLinzpzLidnL(nL)pi(np+d)LinLpinp+dnLLi s i ′ = ( n · L ) p i − L i n x p x − L i n y p y − L i n z p z − L i d n · L ( n · L ) p i − ( n · p + d ) L i n · L p i − n · p + d n · L L i

这正是方程10.1中s的第i个坐标,所以s = s’。

10-8
图10.8 n·L <0的情况。

要使用阴影矩阵,我们将它与我们的世界矩阵结合起来。 然而,在世界变换之后,几何图形还没有真正投影到阴影平面上,因为视角鸿沟尚未出现。 如果sw = n·L <0,则会出现问题,因为这会使w坐标为负数。 通常在透视投影过程中,我们将z坐标复制到w坐标中,并且负w坐标表示该点不在视图体积中,因此被剪切掉(剪切在分割之前的均匀空间中完成)。 这对平面阴影是一个问题,因为除了透视分割之外,我们现在使用w坐标来实现阴影。 图10.8显示了一个有效的情况,其中n·L <0,但阴影不会出现。

为了解决这个问题,我们应该使用矢量朝向无限远的光源LT = -L,而不是使用光线方向L. 观察r(t)= p + tL和r(t)= p + tLT定义相同的3D线,线和平面之间的交点将相同(交点参数ts将不同以补偿 LT和L之间的符号差异)。 所以使用LT = -L给出了相同的答案,但是n·L> 0,这避免了负的w坐标。

10.5.2 点光阴影

图10.9显示了一个物体相对于一个点光源投射的阴影,该点光源的位置由点L描述。点光线通过任何顶点p的光线由下式给出: r(t)=p+t(pL) r ( t ) = p + t ( p − L ) 。射线r(t)与阴影平面(n,d)的交点给出s。通过用平面拍摄对象顶点中的每个顶点所发现的交点集合定义了投影的阴影几何图形。对于顶点p,其阴影投影由下式给出

s=r(ts)=pnp+dn(pL)(pL) s = r ( t s ) = p − n · p + d n · ( p − L ) ( p − L )

10-9
图10.9 相对于点光源投射的阴影。

公式10.2也可以写成矩阵方程:
Spoint=nL+dLxnxLxnyLxnzLxdLynynL+dLynyLynzLydLznzLznynL+dLznzLzdnxnynznL S p o i n t = [ n · L + d − L x n x − L y n y − L z n z − n x − L x n y n · L + d − L y n y − L z n y − n y − L x n z − L y n z n · L + d − L z n z − n z − L x d − L y d − L z d n · L ]

为了了解这个矩阵如何等于公式10.2,我们只需要按照前一节中的方法进行乘法运算。 请注意,最后一列没有零,并给出:
sw=pxnxpynypznznLpn+nLn(pL) s w = p x n x − p y n y − p z n z − n · L − p · n + n · L − n · ( p − L )

这是公式10.2中分母的负数,但如果我们也否定分子,则可以否定分母。

请注意,L为点光源和平行光源提供了不同的用途。 对于点光源,我们使用L来定义点光源的位置。 对于平行光,我们使用L来定义朝向无限远的光源的方向(即,平行光线行进的相反方向)。

10.5.3 一般阴影矩阵

使用齐次坐标,可以创建一个适用于点光源和定向光源的通用阴影矩阵。
1.如果 LW=0 L W = 0 ,则L描述朝向无限远的光源的方向(即,平行光线行进的相反方向)。
2.如果 LW=1 L W = 1 ,那么L描述点光源的位置。
然后我们用下面的阴影矩阵表示从顶点p到其投影s的转换:

S=nL+dLwLxnxLxnyLxnzLxdLynynL+dLwLynyLynzLydLznzLznynL+dLwLznzLzdLxnxnynznL S = [ n · L + d L w − L x n x − L y n y − L z n z − L x n x − L x n y n · L + d L w − L y n y − L z n y − n y − L x n z − L y n z n · L + d L w − L z n z − n z − L x d − L y d − L z d n · L ]

很容易看出,如果 LW=0 L W = 0 ,则S减小到 Sdir S d i r ,并且当 LW=1 L W = 1 时S减少到 Spoint S p o i n t

XNA数学库提供了以下函数来建立阴影矩阵,给出我们希望投影阴影的平面和描述如果w = 0时的平行光或w = 1时的点光的矢量:

XMFINLINE XMMATRIX XMMatrixShadow(
    FXMVECTOR ShadowPlane,
    FXMVECTOR LightPosition);

为了进一步阅读,[Blinn96]和[Möller02]讨论平面阴影。

10.5.4 使用模板缓冲区防止双重混合

当我们将一个物体的几何图形展平到平面上来描述它的阴影时,可能(并且事实上很可能)两个或更多的扁平三角形将重叠。当我们渲染具有透明度的阴影(使用混合)时,这些具有重叠三角形的区域将被混合多次,因此显得较暗。图10.10显示了这一点。

我们可以使用模板缓冲来解决这个问题。
1.假设阴影将被渲染的模板缓冲区像素已被清除为0.这在我们的镜像演示中是真实的,因为我们只将阴影投射到地平面上,而我们只修改镜像模板缓冲区像素。
2.如果模板缓冲区的条目为0,则将模板测试设置为仅接受像素。如果模板测试通过,则我们将模板缓冲区值增加到1。

当我们第一次渲染一个阴影像素时,模板测试会通过,因为模板缓冲区条目是0.但是,当我们渲染这个像素时,我们也将相应的模板缓冲区条目增加到1.因此,如果我们尝试覆盖到 已经渲染到的区域(在模板缓冲区中标记值为1),模板测试将失败。 这可以防止多次绘制相同的像素,从而防止双重混合。

10-10
图10.10 注意左图像阴影中较暗的“粉刺”区域; 这些对应于部分扁平颅骨重叠的区域,从而导致“双重混合”。右侧的图像显示了正确的阴影,没有双重混合。

10.5.5 阴影代码

我们定义了一个阴影材质,用于为仅有50%透明黑色材质的阴影着色:

mShadowMat.Ambient = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
mShadowMat.Diffuse = XMFLOAT4(0.0f, 0.0f, 0.0f, 0.5f);
mShadowMat.Specular = XMFLOAT4(0.0f, 0.0f, 0.0f, 16.0f);

为了防止双重混合,我们设置了以下深度/模板状态对象:

D3D11_DEPTH_STENCIL_DESC noDoubleBlendDesc;
noDoubleBlendDesc.DepthEnable = true;
noDoubleBlendDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
noDoubleBlendDesc.DepthFunc = D3D11_COMPARISON_LESS;
noDoubleBlendDesc.StencilEnable = true;
noDoubleBlendDesc.StencilReadMask = 0xff;
noDoubleBlendDesc.StencilWriteMask = 0xff;
noDoubleBlendDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_INCR;
noDoubleBlendDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// We are not rendering backfacing polygons, so these settings do not matter.
noDoubleBlendDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
noDoubleBlendDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_INCR;
noDoubleBlendDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;
ID3D11DepthStencilState* NoDoubleBlendDSS;
HR(device->CreateDepthStencilState(&noDoubleBlendDesc, &NoDoubleBlendDSS));

然后我们绘制如下的头骨阴影:构建阴影矩阵并使用StencilRef值为0的“非双混合”深度/模板状态进行渲染:

XMVECTOR shadowPlane = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); // xz plane
XMVECTOR toMainLight = -XMLoadFloat3(&mDirLights[0].Direction);
XMMATRIX S = XMMatrixShadow(shadowPlane, toMainLight);
XMMATRIX shadowOffsetY = XMMatrixTranslation(0.0f, 0.001f, 0.0f);
// Set per object constants.
XMMATRIX world = XMLoadFloat4x4(&mSkullWorld)*S*shadowOffsetY;
XMMATRIX worldInvTranspose = MathHelper::InverseTranspose(world);
XMMATRIX worldViewProj = world*view*proj;
Effects::BasicFX->SetWorld(world);
Effects::BasicFX->SetWorldInvTranspose(worldInvTranspose);
Effects::BasicFX->SetWorldViewProj(worldViewProj);
Effects::BasicFX->SetMaterial(mShadowMat);
md3dImmediateContext->OMSetDepthStencilState(RenderStates::NoDoubleBlendDSS, 0);
pass->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mSkullIndexCount, 0, 0);

10.6 总结

1.模板缓冲区是一个屏幕外的缓冲区,我们可以使用它来阻止将某些像素片段渲染到后台缓冲区。 模板缓冲区与深度缓冲区共享,因此具有与深度缓冲区相同的分辨率。 有效深度/模板缓冲区格式为DXGI_FORMAT_D32_FLOAT_S8X24_UINTDXGI_FORMAT_D24_UNORM_S8_UINT

2.阻止特定像素被写入的决定由模板测试决定,其由以下给出:

if(StencilRef & StencilReadMask ⊴ Value & StencilReadMask)
    accept pixel
else
    reject pixel

其中⊴运算符是D3D11_COMPARISON_FUNC枚举类型中定义的任何一个函数。 StencilRef,StencilReadMask,StencilReadMask和比较运算符都是使用Direct3D深度/模板API设置的应用程序定义数量。 值数量是模板缓冲区中的当前值。

3.创建ID3D11DepthStencilState接口的第一步是填写一个D3D11_DEPTH_STENCIL_DESC实例,该实例描述了我们要创建的深度/模板状态。 在填充D3D11_DEPTH_STENCIL_DESC结构后,我们可以使用ID3D11Device :: CreateDepthStencilState方法获取指向ID3D11DepthStencilState接口的指针。 最后,我们使用ID3D11Device :: OMSetDepthStencilState方法将深度/模板状态块绑定到管道的输出合并阶段。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值