1.将实物照常渲染到后台缓冲区内(不包括镜子)。注意,此步骤不修改模 板缓冲区。
2.清理模板缓冲区,将其整体置零。
将实物都绘制到后台缓冲区中,并将模板缓冲区清理为0 (用浅灰色来表示)。
绘制在模板缓冲区中的黑色轮廊线条反映的是:后台缓冲区与模板缓冲区中像素之间的对照关系,而并非模板缓冲区中所绘的实际数据。
3.仅将镜面渲染到模板缓冲区中。若要禁止其他颜色数据写入到后台缓冲区,可用下列设置所创 建的混合状态:
D3D12_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask = 0;
再通过以下配置来禁止向深度缓冲区的写操作:
D3D12_DEPTH_STENCIL_DESC::DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
在向模板缓冲区渲染镜面的时候,我将模板测试设置为每次都成功(D3D12_COMPARISON_ALWAYS), 并且在通过测试时用1 ( StencilRef模板参考值)来替换(通过D3D12_STENCIL_OP_REPLACE来 设置)模板缓冲区元素。如果深度测试失败,则应当采用枚举项D3D12_STENCIL_OP_KEEP,使模板缓冲区中的对应像素保持不变。由于仅向模板缓冲区绘制了镜面,因此在模板缓冲区内,除了镜面可见部分的对应像素为1,其他像素皆为0。图下所示的即为更新后的模板缓冲区。换言之,我们其实就是在模板缓冲区中标记了镜面的可见像素而已。
图把镜面渲染到模板缓冲区中,其实就是在模板缓冲区中标记出镜面可视部分的对应像素。
模板缓冲区中实心黑色区域的模板元素取值为1。但请注意,由于被立方体挡住部分的深度测试会失败,所以在模板缓冲区中的这一范围内,元素的取值并不为1
(立方体与黑色镜面重合的部分,也就是立方体位于镜面前方的这一部分)
保证先绘制实物,后将镜面渲染至模板缓冲区的顺序是很重要的。这样一来,深度 测试的失败会令镜面的像素被实物的像素所遮挡,因而也就不必再对模板缓冲区进 行二次修改了。我们并不希望把模板缓冲区中镜面被遮挡部分的值设为1,那样将导致在实物位于镜面前方的范围内也能显示出镜面内容。
4.现在我们来将实物的镜像渲染至后台缓冲区及模板缓冲区中。前面曾提到,只有通过模板测 试的像素才能渲染至后台缓冲区。对此,我们便将其设置为:仅当模板缓冲区中的值为1时, 才能通过模板测试。这可以通过令StencilRef为1,且模板运算符为D3D12_COMPARISON_ FUNC_EQUAL来实现。如此一来,只有模板缓冲区中元素数值为1的实物镜像部分才能得以 渲染。由于只有镜面可见部分所对应的模板缓冲区中元素数值为1,所以仅有这一范围内的实物镜像才能被渲染出来。
5.最后,我们像往常那样将镜面渲染到后台缓冲区中。但是,为了能“透过”镜面观察实物的 镜像(它实际位于镜子的背面。虽说展现的是镜面内的镜像,但实际上是镜面背后的反射实物 与镜面透明混合所得到的效果),我们就需要运用透明混合技术来渲染镜面。若非如此,则由于 实物镜像的深度值小于镜面的深度值,理所当然地会致使实物镜像被镜子挡住。为此,我 们只需为镜面定义一个新的材质配置实例:将其漫反射alpha通道分量设为0.3,使镜子的不透 明度达到30%,
auto icemirror = std::make_unique<Material>();
icemirror->Name = "icemirror";
icemirror->MatCBIndex = 2;
icemirror->DiffuseSrvHeapIndex = 2;
icemirror->DiffuseAlbedo = XMFLOAT4(1.0f, 1.0f, 1.0f, 0.3f);
icemirror->FresnelR0 = XMFLOAT3(0.1f, 0.1f, 0.1f);
icemirror->Roughness = 0.5f;
假设已经将实物镜像的像素置于后台缓冲区内,那么,此时我们所看到的镜像颜色30%来自镜子 (源像素),70%出自实物镜像(目标像素)。
定义镜像的深度/模板状态
为了实现上述算法,我们要用到两个PSO对象。第一个用于在绘制镜面时标记模板缓冲区内镜面部 分的像素,第二个则用于绘制镜面可见部分(即不被前侧实物所遮挡部分)内的实物镜像。
//
//
// PSO for marking stencil mirrors.禁止对渲染目标的写操作
//
CD3DX12_BLEND_DESC mirrorBlendState(D3D12_DEFAULT);
mirrorBlendState.RenderTarget[0].RenderTargetWriteMask = 0; //禁止对渲染目标的写操作
D3D12_DEPTH_STENCIL_DESC mirrorDSS;
mirrorDSS.DepthEnable = true;
mirrorDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;//禁止对渲染目标的写操作
mirrorDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
mirrorDSS.StencilEnable = true;
mirrorDSS.StencilReadMask = 0xff;
mirrorDSS.StencilWriteMask = 0xff;
mirrorDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE;
mirrorDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
//我们不渲染背面朝向的多边形,因而对这些参数血置并不关心
// We are not rendering backfacing polygons, so these settings do not matter.]
mirrorDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
mirrorDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_REPLACE;
mirrorDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
D3D12_GRAPHICS_PIPELINE_STATE_DESC markMirrorsPsoDesc = opaquePsoDesc;
markMirrorsPsoDesc.BlendState = mirrorBlendState;
markMirrorsPsoDesc.DepthStencilState = mirrorDSS;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&markMirrorsPsoDesc, IID_PPV_ARGS(&mPSOs["markStencilMirrors"])));
//
// PSO for stencil reflections.用于渲染模板缓冲区中反射镜像的PSO
//
D3D12_DEPTH_STENCIL_DESC reflectionsDSS;
reflectionsDSS.DepthEnable = true;
reflectionsDSS.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
reflectionsDSS.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
reflectionsDSS.StencilEnable = true;
reflectionsDSS.StencilReadMask = 0xff;
reflectionsDSS.StencilWriteMask = 0xff;
reflectionsDSS.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
// We are not rendering backfacing polygons, so these settings do not matter.
reflectionsDSS.BackFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.BackFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.BackFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
reflectionsDSS.BackFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
D3D12_GRAPHICS_PIPELINE_STATE_DESC drawReflectionsPsoDesc = opaquePsoDesc;
drawReflectionsPsoDesc.DepthStencilState = reflectionsDSS;
drawReflectionsPsoDesc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK;
drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&drawReflectionsPsoDesc, IID_PPV_ARGS(&mPSOs["drawStencilReflections"])));
绘制场景
//绘制不透明的物体
// Draw opaque items--floors, walls, skull.
auto passCB = mCurrFrameResource->PassCB->Resource();
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]);
//将模板缓冲区中可见的镜面像素标记为1
Mark the visible mirror pixels in the stencil buffer with the value 1
mCommandList->OMSetStencilRef(1);
mCommandList->SetPipelineState(mPSOs["markStencilMirrors"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Mirrors]);
//只绘制镜子范围内的镜像(即仅绘制模板缓冲区中标记为1的像素)
//注意,我们必须使用两个单独的渲染过程常量缓冲区(per-pass constant buffer)来完成此工作,
//一个存储物体镜像,另一个保存光照镜像
// Draw the reflection into the mirror only (only for pixels where the stencil buffer is 1).
// Note that we must supply a different per-pass constant buffer--one with the lights reflected.
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress() + 1 * passCBByteSize);
mCommandList->SetPipelineState(mPSOs["drawStencilReflections"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Reflected]);
//恢复主渲染过程常量数据以及模板参考值
// Restore main pass constants and stencil ref.
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
mCommandList->OMSetStencilRef(0);
//绘制透明的镜面,使镜像可以与之混合
// Draw mirror with transparency so reflection blends through.
mCommandList->SetPipelineState(mPSOs["transparent"].Get());
DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Transparent]);
关于以上代码还有一点需要注意,即在绘制RenderLayer: :Reflected层的时候如何来修改其渲染过程常量缓冲区。这是因为在绘制物体镜像的同时,还涉及场景中光照的镜像(即,物体的镜像也 要有与之对应的光照)。光源本存于渲染过程常量缓冲区中,因此我们可以再额外创建一个渲染过程常量 缓冲区,用以存储场景中光照的镜像。该常量缓冲区的设置方法如下:
PassConstants mMainPassCB;
PassConstants mReflectedPassCB;
void StencilApp::UpdateReflectedPassCB(const GameTimer& gt)
{
mReflectedPassCB = mMainPassCB;
XMVECTOR mirrorPlane = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f); // xy plane
XMMATRIX R = XMMatrixReflect(mirrorPlane);
// 光照镜像
for(int i = 0; i < 3; ++i)
{
XMVECTOR lightDir = XMLoadFloat3(&mMainPassCB.Lights[i].Direction);
XMVECTOR reflectedLightDir = XMVector3TransformNormal(lightDir, R);
XMStoreFloat3(&mReflectedPassCB.Lights[i].Direction, reflectedLightDir);
}
// 将光照镜像的渲染过程常量数据存于渲染过程常量缓冲区中索引1的位置
auto currPassCB = mCurrFrameResource->PassCB.get();
currPassCB->CopyData(1, mReflectedPassCB);
}
绕序与镜像
当一个三角形被反射到某个平面上时(也就是此三角形在这一平面上的镜像),其绕序(winding order ) 并不会发生改变,正因如此,其平面法线的方向同样保持不变。所以,实际物体的外向法线在镜像中则变为 了内向法线。此时,为了纠正这一点,我们会告知Direct3D将逆时针绕序的三角形看作是正面 朝向,而将顺时针绕序的三角形看作背面朝向。这实际上 是对法线的方向也进行了 “反射”,以此使镜像成为外向朝向。我们可以通过设置下列PSO光栅化属性来改 变绕序的约定:
drawReflectionsPsoDesc.RasterizerState.FrontCounterClockwise = true;