(转)【D3D11游戏编程】学习笔记十九:平面阴影的渲染

(注:【D3D11游戏编程】学习笔记系列由CSDN作者BonChoix所写,转载请注明出处:http://blog.csdn.net/BonChoix,谢谢~)

       在这一篇文章中,我们讨论的话题依然是模板缓冲区,这次通过另一个十分常见的例子:阴影,继续来感受一下模板缓冲区灵活的用法。

       阴影的渲染是个很高级的话题,当然,我们这次仅仅讨论平面阴影,即光源照射物体在平面上投下的阴影,直接进入正题。

       同镜子的渲染一样,平面阴影的渲染也包括两个关键问题:

       1. 阴影矩阵的生成

       2. 模板缓冲区的设置

       1. 阴影矩阵

       在了解如何获得相应的阴影矩阵之前,先来看下生成阴影的两种常见的光源照射方式:

                              

          如以上两张图所示,左边为平行光照射下一个三角形及其阴影,右边为点光源照射下的三角形及其阴影。两个三角形中各有一个p点,在两种光源的照射下阴影分别投在了平面上的s点。直观上来讲,对于一个点,它在平面上的阴影位置是这样获得的:即从该点开始,沿通过该点的光线方向形成的射线与平面的交点。换句话说,给定一个点,只要知道了通过该点的光线方向, 即可获得该点在特定平面的阴影位置。因此:阴影矩阵的生成只取决于两个因素:光线方向和投影平面。

       对于平行光情形,对于所有点,光源方向是固定的;而对于点光源情形,针对每个点,通过该点的光线方向是变化的,该方向与光源所在位置有关。给定一个点p,及光源位置P0,通过该点的的光线方向为p - P0。因此对于任一点,只要给了光源位置,它在平面上的阴影位置也是可以求得的。

       综上所述,针对平行光与点光源两种照射模型,获得阴影矩阵的不同之处仅在于:平行光需要知道光源照射方向,而点光源需要知道光源位置。此外,它们共同的条件是阴影平面。事实上,无论是平行光的方向,还是点光源的位置,都是用4维向量表示的,这就使得我们可以以一致的方式来获得阴影矩阵。在XNA数学库中,获得阴影矩阵的函数如下:

[html] view plain copy
  1. XMMATRIX XMMatrixShadow(  
  2.          XMVECTOR ShadowPlane,  
  3.          XMVECTOR LightPosition  
  4. )  

第一个参数为投影平面,上次我们说过,可以用4维向量表示;

第二个参数,对于平行光即代表其方向,对于点光源即代表其位置。不同之处在于该向量的w分量是0还是1。

此外,针对平行光有一点要注意:当使用第二个参数表示平行光的光源方向时,该向量必须为真实光源方向的反方向。

比如,如果光源方向为[1,0,0,0],则该参数应为[-1,0,0,0]。这主要是由于矩阵生成过程中点坐标的w分量导致的,如果直接写成光源方向,会导致阴影点的w分量变负,对于w为负数的点在渲染管线中的裁剪阶段是会直接剔除掉的。如果想更深入地了解,可以参考3D数学相关书籍中阴影矩阵的推导部分。

       到现在为止,阴影矩阵的问题就解决了。

       2. 借助模板解决Double Blending问题

       第二个重要问题即“Double Blending”问题。针对这个概念,请看下图所示:

       该图简单地展示了一个物体在平行光照射下阴影的生成。图中,n为投影平面的法线,L为光源照射方向。我们着重考虑多边形中的A,B,C,D四个点在平面上的投影。我们可以看到,A,B两个点正好投影在平面上的同一点P1,C、D两点各自投影到P2、P2两点。这样,在使用阴影矩阵渲染该多边形的阴影时,由于A、B对应于同一个阴影点,因此P1点可能被渲染两次;而对于C、D两点,由于各只有一个点被投影到平面上,因此P2、P3各渲染一次。同理,对于阴影上的其他点,也可能由于物体上有多个点被投影到该点上,而造成该点被渲染多次。

       由于阴影的渲染实际上是通过使用黑色材质与场景中对应该点的原颜色值混合得到的,这样就会导致一个问题,即被渲染多次的阴影点会比渲染一次的阴影点由于多次的混合而颜色更加深一点。这种问题即我们说的“Double Blending”。

       我们希望的情况是:对于阴影上的每一点,只被渲染一次,如果物体中还有其他点被投影到阴影上的同一点,则该点被丢弃,而不再进行渲染。要实现这种效果,就又要用到强大的模板缓冲啦。在渲染镜子时,我们用模板来标记镜子的范围,在渲染阴影时,我们则用它来标记阴影上被渲染过的点,以告诉其他点:“我已经被渲染过一次了,请不要再渲染我了!”。

       基于这种设想,我们可以这样来设置模板缓冲区:

       1. 一开始清屏时,模板缓冲区统一为0;

       2. 当渲染阴影时,开启模板功能,对于每个要渲染的点,判断它对应的模板值是否为0,如果是,则渲染它,并且把模板值加1(或其他任意值,我们的目的是改变模板值);如果一个点对应的模板值不为0,则说明它已经被渲染过了(否则它的模板值怎么会变嘛),从而丢弃它。

       针对D3D11,我们使用的比较函数为EQUAL,与模板参考值相等的才通过模板测试;通过模板测试的更新操作为INCR(INCR_SAT、REPLACE也是可以的!),测试失败的操作为KEEP,不改变模板值。

       相关配置代码如下(注意,BackFace我们是不关心的):

[cpp] view plain copy
  1. //平面阴影渲染设置  
  2. D3D11_DEPTH_STENCIL_DESC noDoubleBlendDesc;  
  3. noDoubleBlendDesc.DepthEnable = true;  
  4. noDoubleBlendDesc.DepthFunc = D3D11_COMPARISON_LESS;  
  5. noDoubleBlendDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;  
  6. noDoubleBlendDesc.StencilEnable = true;  
  7. noDoubleBlendDesc.StencilReadMask = 0xff;  
  8. noDoubleBlendDesc.StencilWriteMask = 0xff;  
  9. noDoubleBlendDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;  
  10. noDoubleBlendDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_INCR;  
  11. noDoubleBlendDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;  
  12. noDoubleBlendDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;  
  13. noDoubleBlendDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;  
  14. noDoubleBlendDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;  
  15. noDoubleBlendDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;  
  16. noDoubleBlendDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;  

       在程序框架中,我们把该状态定义为"NoDoubleBlendingDSS",以方便在程序中直接使用。

       最后,还有一点要注意:尽管我们把阴影投在地面上,但在渲染阴影时,要把阴影位置稍微从地面上提高一点点。否则,由于阴影与地面完全重合,它们将会具有相同的深度值,从而导致在Output Merger阶段发生竞争,造成闪烁的效果。

       在下面的代码中计算worldShadow矩阵时0.001f的偏移即是针对这一点的。

[cpp] view plain copy
  1. //绘制阴影  
  2. m_deviceContext->IASetVertexBuffers(0,1,&m_VBBox,&stride,&offset);  
  3. m_deviceContext->IASetIndexBuffer(m_IBBox,DXGI_FORMAT_R32_UINT,0);  
  4. //地面平面的数学表示:【0.f,1.f,2.f,2.5f】  
  5. XMVECTOR ground = XMVectorSet(0.f,1.f,0.f,2.5f);  
  6. //投影方向:光源方向的反向  
  7. XMVECTOR lightDir = -XMLoadFloat3(&m_dirLights[0].dir);  
  8. //生成投影矩阵  
  9. XMMATRIX S = XMMatrixShadow(ground,lightDir);  
  10. //箱子阴影 相关变换矩阵  
  11. XMMATRIX worldShadow = XMLoadFloat4x4(&m_worldBox) * S * XMMatrixTranslation(0.f,0.001f,0.f);  
  12. XMMATRIX worldInvTransposeShadow = InverseTranspose(worldShadow);  
  13. XMMATRIX wvpShadow = worldShadow * view * proj;  
  14. XMMATRIX texTransShadow = XMMatrixIdentity();  
  15. //设置好模板状态:NoDoubleBlending  
  16. m_deviceContext->OMSetDepthStencilState(RenderStates::NoDoubleBlendingDSS,0x0);  
  17. Effects::fxBasic->SetMaterial(m_materialShadow);  
  18. Effects::fxBasic->SetWorldMatrix(worldShadow);  
  19. Effects::fxBasic->SetWorldInvTransposeMatrix(worldInvTransposeShadow);  
  20. Effects::fxBasic->SetWorldViewProjMatrix(wvpShadow);  
  21. Effects::fxBasic->SetTextureTransform(texTransShadow);  
  22. //绘制箱子阴影  
  23. for(UINT i=0; i<tech2Desc.Passes; ++i)  
  24. {  
  25.     tech2->GetPassByIndex(i)->Apply(0,m_deviceContext);  
  26.     m_deviceContext->DrawIndexed(m_box.indices.size(),0,0);  
  27. }  
  28. //恢复状态  
  29. m_deviceContext->OMSetDepthStencilState(0,0);  

       首先要计算当前的阴影矩阵,并更新Effect中的变量;然后开启相应的模板状态,渲染箱子即可;最后把模板状态恢复到默认。

       平面阴影的渲染就是这样,so easy!

       以下是示例程序的一张运行效果图:

       最后,我还想再提一下,这篇文章中我们学习的仅仅是阴影渲染的最简单的一种情形:平面阴影。因此,看到上面这张图,可不要指望当箱子距离墙面很近时,影子会投到墙上啊。。。。尽管按实际情况确实应该投在墙上,但对于这里的平面阴影,我们只针对地面所在平面进行了投影变换,因此即使箱子离墙面很近,影子也会“穿过墙面”而投在地面上的! 是不是觉得这个程序中的地面比上次镜子示例程序中地面大了?没错,就是专门为了避免箱子会把影子投到墙上而造成的尴尬,我才把地面扩大,并把箱子往外面挪了一下。

       当然,简单归简单,作为第一次学习使用模板来渲染阴影的例子,还是很有用的。更高级的阴影渲染技术,比如Shadow Volume,同样会使用到模板,我们在后面会学习到的。

       最后是本次阴影渲染示例程序

       本文完。

原文地址:https://www.cnblogs.com/wodehao0808/p/6603944.html