倩女幽魂端游阴影优化

未来已来2018-09-10 12:46

作者:鲁林


在实时场景绘制中,shadowmap是一种非常常见的阴影绘制方法,其基本方法包含两个步骤:首先将摄像机放在光源位置绘制场景,本次绘制可以获得深度信息并记录在一张2D纹理中;再将摄像机放在视线位置绘制场景,利用第一遍绘制得到的深度信息,判断物体是否处于阴影之中。

  这种方法简单高效,但是在场景较大,shadow map分辨率不高时,会产生锯齿。其根本原因在于多个像素点会对应到同一个shadow map UV,那么这些点采样出来的深度值是相同的。
  倩女的场景相对较大,一味增加shadow map分辨率并不能很好的解决问题,那么怎么解决这个问题呢?在实际工作中,采用了一种PSSM的方法(Parallel-Split Shadow Maps)。简单来说就是绘制多个shadow map,根据顶点位置选择不同的shadow map。
  一个非常直观的印象是,绘制场景时,根据像素点与摄像机的距离不同,所需要的shadow map采样密度也不同。像素点远离摄像机,我们可以采样低密度的shadow map,像素点离摄像机近,我们就需要采集密度高一些的shadow map,这也是PSSM的原理。


  如图上图所示,我们把视锥切割成多份,针对每一个视锥做一次绘制,生成一张shadow map,最终绘制像素时,我们可以根据距离采样不同的shadow map。
  怎么分割视锥便是我们首先需要解决的问题,如下图:

shadow map中锯齿误差的主要来源在于dp/ds,而这一误差可以表示为以下公式:

其中dz/zds是透视所产生的误差,是可以优化的部分

在实际操作中,采用了如下的分割公式


其中  表示按指数切割,计算方式如下  这一部分是为了优化透视误差在near plane到far plane之间的分布

表示平均切割,计算公式为 这一部分是为中和指数分割的过采样和采样不足


理论说完了,回到代码看一下!

void CShadowMap::CalSplitZ(float neardis,float fardis)
{
    m_SplitZ[0] = neardis;
    for(int i = 1; i < 3; ++i)
    {
        float coef = i / 3.0f;
        float lerpAve = neardis + coef * (fardis - neardis);
        float lerpLog = neardis * powf(fardis/neardis, coef);
        float SplitsZ =  (1-m_fPSSMBias) * lerpAve  + m_fPSSMBias * lerpLog; 
        m_SplitZ[i] = SplitsZ;
    }
    m_SplitZ[3] = fardis;
}

  实践中,我们将视锥切割为3个部分m_SplitZ[0]为near plane,m_SplitZ[3]为far plane,中间两个切面位置由以上公式得出,λ也就是m_fPSSMBias,我们取的是0.65(经验值)。
  此外,我们也要将光源处的视锥进行切分,计算它与视线视锥的包围盒,由此计算得到光源处视锥的投影矩阵。

void CShadowMap::CalLightMatrix()
{
      .......    
    for(int t = 0 ; t<4 ;++t)
    {
        D3DXVec3Transform(&point, &FPoint[t][0], &matViewProj);
        minx = maxx = point.x; miny = maxy = point.y; minz = maxz = point.z;
        for (int i=1; i<8; i++)
        {
            D3DXVec3Transform(&point, &FPoint[t][i], &matViewProj);
            minx = minx>point.x?point.x:minx;
            miny = miny>point.y?point.y:miny;
            minz = minz>point.z?point.z:minz;
            maxx = maxx<point.x?point.x:maxx;
            maxy = maxy<point.y?point.y:maxy;
            maxz = maxz<point.z?point.z:maxz;
        }
        if (t!=0)
            minz = minz-(4-t)*0.5; 
        minz -=(maxz-minz);
        D3DXVECTOR3 Scale, Offset;
        Scale.x = 2.0f/(maxx-minx);
        Scale.y = 2.0f/(maxy-miny);
        Scale.z = 1.0f/(maxz-minz);
        Offset.x = -0.5f*(minx+maxx)*Scale.x;
        Offset.y = -0.5f*(miny+maxy)*Scale.y;
        Offset.z = -minz*Scale.z;
        D3DXMatrixTransformation(Crop[t], NULL, NULL, &Scale, NULL, NULL, &Offset);            
        D3DXMatrixMultiply(CropViewProj[t], &m_matView, &m_matProj);
        D3DXMatrixMultiply(CropViewProj[t], CropViewProj[t], Crop[t]);
    }
}

  有了以上部分,我们就可以分别绘制多个视锥下的shadowmap了。 这些shadowmap绘制完毕后,就可以在pass 2中使用它们绘制场景。根据像素点和near plane的距离,采样不同的shadow map。

float ShadowCoefPSSM(float4 Inpos)
{
    float viewz = mul(float4(Inpos.xyz, 1.0f), matWorldView).z;
    float density = 0.0f;
    for(int i = 0; i < 3; i++)  
    { 
        if(viewz < splitz[i])
        {
            float4 pos = mul(float4(Inpos.xyz, 1.0f), matLightCropMatrixPSSM[i]);
            density += tex2Dproj(ShadowMapSampler[i], pos);
            break;
        }
    }
    return density;    
}

  最后我们看一下优化的效果:
先上一张未优化的,可以看到摄像头拉近后,shadow map分辨率严重不足,虽然做了简单的抗锯齿,但效果还是不好。


  我们再看优化后的,摄像头分别由远及近:




  很明显,根据像素点和采样了不同的shadow map,阴影效果有了较大改善。


本文来自网易实践者社区,经作者鲁林授权发布