3D游戏引擎系列(十二):实时阴影渲染

发表于2017-04-04
评论0 2.7k浏览

对于3D游戏产品都需要阴影技术的实现,阴影的运行效率也成为判定游戏研发技术水平的手段之一。游戏中实现阴影的方式有很多种,主要分三种:一种是对于静态物体比如建筑物可以使用LightMap渲染,将建筑的阴影直接渲染到地面上这种技术广泛应用在移动端,Unity引擎本身就提供了此功能。另一种是对于游戏中动态的物体,实现方式是在移动端或者在网页游戏中为了优化效率,直接用一张带有Alpha通道的贴图放到角色的下面,可以实时跟随角色移动。第三种实现方式是该书重点讲解的实时阴影渲染,实时阴影在PC端游特别是次时代网游中很常见,鉴于PC端硬件的强大处理能力,应用实时阴影技术对整个游戏场景进行渲染,为的是增加游戏场景的真实性。当然实时阴影技术的运用会对CPU和GPU有一定的消耗,所以对于实时阴影的渲染,可以通过摒弃掉不需要实时渲染的建筑物进行效率优化。实时渲染技术常用的是PSSM(Parallel-Split ShadowMap)算法,实现阴影的算法非常多的,我就不一一列举了。PSSM通过字面意思知道就是平行切分视锥,游戏中实时阴影的渲染效果如下图:


要实现如此的效果,得从PSSM实现的原理讲起,PSSM算法的核心就是把视椎体进行分割,然后分别渲染组合。语言讲解不如看图直观,先通过视锥体分割说起。效果如下图:


PSSM实时阴影的绘制首先需要灯光,在现实生活中,白天只有太阳出来了才可以看到影子。在虚拟世界中也是一样的,场景使用的是Directional(平行光)相当于现实世界的太阳光。上图左边部分显示的是视景体的投影,利用PSSM算法将其平行的分割成多个部分,然后对每个部分进行渲染,分割成的块数是可以自己设置的。右半部分是顶视角观看的分割效果,把物体分成三块进行实时阴影的渲染。渲染的计算是GPU中执行的,在GPU中执行的流程如下图:


        上图的处理流程首先是场景中的灯光照射到需要投影的物体上,接下来程序对投影的物体顶点进行矩阵变换将其转换到投影空间中,再转换到裁剪空间进行视口的平行分割,最后将其分别渲染出来。渲染阴影流程讲完了接下来解决Shader渲染的问题,我们把平行分割的计算放到GPU中执行,需要编写Shader脚本文件,新建一个文本文件把其扩展名字改成.fx。Shader的完整内容如下:

  1. float4x4 g_mViewProj;  
  2.   
  3. void VS_RenderShadowMap(  
  4.   float4 vPos : POSITION,  
  5.   out float4 vPosOut : POSITION,  
  6.   out float3 vPixelOut : TEXCOORD0)  
  7. {  
  8.   // pass vertex position through as usual  
  9.   vPosOut = mul(vPos, g_mViewProj);  
  10.   // output pixel pos  
  11.   vPixelOut=vPosOut.xyz;  
  12. }  
  13.   
  14. float4 PS_RenderShadowMap(float3 vPixelPos : TEXCOORD0): COLOR  
  15. {  
  16.   // write z coordinate (linearized depth) to texture  
  17.   return vPixelPos.z;  
  18. }  
  19.   
  20. // This technique is used when rendering meshes to the shadowmap  
  21. //   
  22. technique RenderShadowMap  
  23. {  
  24.   pass p0  
  25.   {  
  26.     // render back faces to hide artifacts  
  27.     CullMode = CW;  
  28.     VertexShader = compile vs_2_0 VS_RenderShadowMap();  
  29.     PixelShader = compile ps_2_0 PS_RenderShadowMap();  
  30.   }  
  31. }  
  32.   
  33. float3 g_vLightDir;  
  34. float3 g_vLightColor;  
  35. float3 g_vAmbient;  
  36. float g_fShadowMapSize;  
  37. float4x4 g_mShadowMap;  
  38.   
  39. // no filtering in floating point texture  
  40. sampler2D g_samShadowMap  =  
  41. sampler_state  
  42. {  
  43.   MinFilter = Point;  
  44.   MagFilter = Point;  
  45.   MipFilter = None;  
  46.   AddressU = Border;  
  47.   AddressV = Border;  
  48.   BorderColor = 0xFFFFFFFF;  
  49. };  
  50.   
  51.   
  52. void VS_Shadowed(  
  53.   in float4 vPos : POSITION,  
  54.   in float3 vNormal : NORMAL,  
  55.   in float fAmbientIn : TEXCOORD0,  
  56.   out float4 vPosOut : POSITION,  
  57.   out float4 vShadowTex : TEXCOORD0,  
  58.   out float fAmbientOut : TEXCOORD1,  
  59.   out float3 vDiffuse : COLOR0)  
  60. {  
  61.   // pass vertex position through as usual  
  62.   vPosOut = mul(vPos, g_mViewProj);  
  63.   
  64.   // calculate per vertex lighting  
  65.   vDiffuse = g_vLightColor * saturate(dot(-g_vLightDir, vNormal));  
  66.   
  67.   // coordinates for shadowmap  
  68.   vShadowTex = mul(vPos, g_mShadowMap);  
  69.   
  70.   // ambient occlusion  
  71.   fAmbientOut = saturate(0.5f+fAmbientIn);  
  72. }  
  73.   
  74. float4 PS_Shadowed(  
  75.   float4 vShadowTex : TEXCOORD0,  
  76.   float fAmbientOcclusion : TEXCOORD1,  
  77.   float4 vDiffuse : COLOR0) : COLOR  
  78. {  
  79.   
  80.   float fTexelSize=1.0f/g_fShadowMapSize;  
  81.   
  82.   // project texture coordinates  
  83.   vShadowTex.xy/=vShadowTex.w;  
  84.   
  85.   // 2x2 PCF Filtering  
  86.   //   
  87.   float fShadow[4];  
  88.   fShadow[0] = (vShadowTex.z < tex2D(g_samShadowMap, vShadowTex).r);  
  89.   fShadow[1] = (vShadowTex.z < tex2D(g_samShadowMap, vShadowTex + float2(fTexelSize,0)).r);  
  90.   fShadow[2] = (vShadowTex.z < tex2D(g_samShadowMap, vShadowTex + float2(0,fTexelSize)).r);  
  91.   fShadow[3] = (vShadowTex.z < tex2D(g_samShadowMap, vShadowTex + float2(fTexelSize,fTexelSize)).r);  
  92.   
  93.   float2 vLerpFactor = frac(g_fShadowMapSize * vShadowTex);  
  94.   float fLightingFactor = lerp(lerp( fShadow[0], fShadow[1], vLerpFactor.x ),  
  95.                                lerp( fShadow[2], fShadow[3], vLerpFactor.x ),  
  96.                                vLerpFactor.y);  
  97.   
  98.   // multiply diffuse with shadowmap lookup value  
  99.   vDiffuse*=fLightingFactor;  
  100.   
  101.   // final color  
  102.   float4 vColor=1;  
  103.   vColor.rgb = saturate(g_vAmbient*fAmbientOcclusion + vDiffuse).rgb;  
  104.   return vColor;  
  105. }  
  106.   
  107. // This technique is used to render the final shadowed meshes  
  108. //  
  109. technique Shadowed  
  110. {  
  111.   pass p0  
  112.   {  
  113.     /   / render front faces  
  114.     CullMode = CCW;  
  115.       VertexShader = compile vs_2_0 VS_Shadowed();  
  116.       PixelShader = compile ps_2_0 PS_Shadowed();  
  117.   }  
  118. }  

理论讲了很多,Shader代码实现起来比较简单,为了消除阴影锯齿,使用了PCF Filtering过滤技术。其他的代码跟以前讲的很类似这里就不一一分析了。接下来通过C++函数接口将参数传递给Shader文件,C++代码核心函数实现如下所示:

  1. void RenderScene(D3DXMATRIX &mView, D3DXMATRIX &mProj)  
  2. {  
  3.   // Set constants  
  4.   //  
  5.   D3DXMATRIX mViewProj=mView * mProj;  
  6.   _pEffect->SetMatrix("g_mViewProj",&mViewProj);  
  7.   
  8.   _pEffect->SetVector("g_vLightDir",&_vLightDir);  
  9.   _pEffect->SetVector("g_vLightColor",&_vLightDiffuse);  
  10.   _pEffect->SetVector("g_vAmbient",&_vLightAmbient);  
  11.   _pEffect->SetFloat("g_fShadowMapSize",(FLOAT)_iShadowMapSize);  
  12.   
  13.   // enable effect  
  14.   unsigned int iPasses=0;  
  15.   _pEffect->Begin(&iPasses,0);  
  16.   
  17.   // for each pass in effect   
  18.   for(unsigned int i=0;i
  19.   {  
  20.     // start pass  
  21.     _pEffect->BeginPass(i);  
  22.     {  
  23.       // for each subset in mesh  
  24.       for(DWORD j=0;j<_iMeshMaterials;j++)  
  25.       {  
  26.         // draw subset  
  27.         _pMesh->DrawSubset(j);  
  28.       }  
  29.     }  
  30.     // end pass  
  31.     _pEffect->EndPass();  
  32.   }  
  33.   // disable effect  
  34.   _pEffect->End();  
  35. }  

该函数主要是将Shader文件中需要使用的参数通过C++代码传递给GPU进行渲染,在介绍PSSM原理时对物体进行Split操作。在C++中的函数如下所示:

[cpp] view plain copy
 
  1. void CalculateSplitDistances(void)  
  2. {  
  3.   // Reallocate array in case the split count has changed  
  4.   //  
  5.   delete[] _pSplitDistances;  
  6.   _pSplitDistances=new float[_iNumSplits+1];  
  7.   _fSplitSchemeLambda=Clamp(_fSplitSchemeLambda,0.0f,1.0f);  
  8.   
  9.   for(int i=0;i<_iNumSplits;i++)  
  10.   {  
  11.     float fIDM=i/(float)_iNumSplits;  
  12.     float fLog=_fCameraNear*powf((_fCameraFar/_fCameraNear),fIDM);  
  13.     float fUniform=_fCameraNear+(_fCameraFar-_fCameraNear)*fIDM;  
  14.     _pSplitDistances[i]=fLog*_fSplitSchemeLambda+fUniform*(1-_fSplitSchemeLambda);  
  15.   }  
  16.   
  17.   // make sure border values are right  
  18.   _pSplitDistances[0]=_fCameraNear;  
  19.   _pSplitDistances[_iNumSplits]=_fCameraFar;  
  20. }  
        最后将上述实现的两个关键函数在Render函数中调用,完成最终的代码实现。渲染函数如下所示:
[cpp] view plain copy
 
  1. void Render(void)  
  2. {  
  3.   // move camera, adjust settings, etc..  
  4.   DoControls();  
  5.   
  6.   // calculate the light position  
  7.   _vLightSource=D3DXVECTOR3(-200*sinf(_fLightRotation),120,200*cosf(_fLightRotation));  
  8.   _vLightTarget=D3DXVECTOR3(0,0,0);  
  9.   // and direction  
  10.   _vLightDir=D3DXVECTOR4(_vLightTarget-_vLightSource,0);  
  11.   D3DXVec4Normalize(&_vLightDir,&_vLightDir);  
  12.   
  13.   // calculate camera aspect  
  14.   D3DPRESENT_PARAMETERS pp=GetApp()->GetPresentParams();  
  15.   float fCameraAspect=pp.BackBufferWidth/(float)pp.BackBufferHeight;  
  16.   
  17.   AdjustCameraPlanes();  
  18.   CalculateSplitDistances();  
  19.   // Clear the screen  
  20.   //  
  21.   GetApp()->GetDevice()->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DXCOLOR(0.5f,0.5f,0.5f,0.5f), 1.0f, 0);  
  22. for(int iSplit=0;iSplit<_iNumSplits;iSplit++)  
  23.   {  
  24.     // use numpad to skip rendering  
  25.     if(GetKeyDown(VK_NUMPAD1+iSplit)) continue;  
  26.   
  27.     // near and far planes for current frustum split  
  28.     float fNear=_pSplitDistances[iSplit];  
  29.     float fFar=_pSplitDistances[iSplit+1];  
  30.   
  31.     // Calculate corner points of frustum split  
  32.   
  33.     float fScale=1.1f;  
  34.     D3DXVECTOR3 pCorners[8];  
  35.     CalculateFrustumCorners(pCorners,_vCameraSource,_vCameraTarget,_vCameraUpVector,  
  36.                             fNear,fFar,_fCameraFOV,fCameraAspect,fScale);  
  37.   
  38.     // Calculate view and projection matrices  
  39.     CalculateLightForFrustum(pCorners);  
  40.   
  41.   
  42.     // Enable rendering to shadowmap  
  43.     _ShadowMapTexture.EnableRendering();  
  44.     // Clear the shadowmap  
  45.     GetApp()->GetDevice()->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, 0xFFFFFFFF, 1.0f, 0);  
  46.   
  47.     // Set up shaders  
  48.     // To hide artifacts, only render back faces of the scene  
  49.     _pEffect->SetTechnique("RenderShadowMap");  
  50.   
  51.     // Render the scene to the shadowmap  
  52.     RenderScene(_mLightView,_mLightProj);  
  53.   
  54.     // Go back to normal rendering  
  55.     _ShadowMapTexture.DisableRendering();  
  56.     /////////////////////////////////////////////////////////////  
  57.     // At this point we have the shadowmap texture rendered.   //  
  58.     /////////////////////////////////////////////////////////////  
  59.   
  60.     // Calculate a matrix to transform points to shadowmap texture coordinates  
  61.     // (this should be exactly like in your standard shadowmap implementation)  
  62.   
  63.     float fTexOffset=0.5f+(0.5f/(float)_iShadowMapSize);  
  64.   
  65.     D3DXMATRIX mTexScale(   0.5f,               0.0f,      0.0f,   0.0f,  
  66.                             0.0f,              -0.5f,      0.0f,   0.0f,  
  67.                             0.0f,               0.0f,      1.0f,   0.0f,  
  68.                             fTexOffset,    fTexOffset,     0.0f,   1.0f );  
  69.   
  70.     D3DXMATRIX mShadowMap=_mLightView * _mLightProj * mTexScale;  
  71.   
  72.     // store it to the shader  
  73.     _pEffect->SetMatrix("g_mShadowMap",&mShadowMap);  
  74.   
  75.     // Since the near and far planes are different for each  
  76.     // rendered split, we need to change the depth value range  
  77.     // to avoid rendering over previous splits  
  78.     D3DVIEWPORT9 CameraViewport;  
  79.     GetApp()->GetDevice()->GetViewport(&CameraViewport);  
  80.     // as long as ranges are in order and don't overlap it should be all good...  
  81.     CameraViewport.MinZ=iSplit/(float)_iNumSplits;  
  82.     CameraViewport.MaxZ=(iSplit+1)/(float)_iNumSplits;  
  83.     GetApp()->GetDevice()->SetViewport(&CameraViewport);  
  84.   
  85.     // use the current splits near and far plane  
  86.     // when calculating matrices for the camera  
  87.     CalculateViewProj(_mCameraView, _mCameraProj,  
  88.                       _vCameraSource,_vCameraTarget,_vCameraUpVector,  
  89.                       _fCameraFOV, fNear, fFar, fCameraAspect);  
  90.   
  91.     // setup shaders  
  92.     _pEffect->SetTechnique("Shadowed");  
  93.     // bind shadowmap as a texture  
  94.     GetApp()->GetDevice()->SetTexture(0,_ShadowMapTexture.GetColorTexture());  
  95.   
  96.     // render the final scene  
  97.     RenderScene(_mCameraView, _mCameraProj);  
  98.   
  99.     // unbind texture so we can render on it again  
  100.     GetApp()->GetDevice()->SetTexture(0,NULL);  
  101.   
  102.     // draw the shadowmap texture to HUD  
  103.     RenderSplitOnHUD(iSplit);  
  104.   }  
  105.   
  106.   // render other HUD stuff  
  107.   RenderHUD();  
  108. }  

         整个PSSM的核心代码就实现完成了,最后本书实现了9级平行分割对物体阴影的实现,实现效果如下:

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引