作者:鲁林
在实时场景绘制中,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,阴影效果有了较大改善。
本文来自网易实践者社区,经作者鲁林授权发布