实时阴影渲染(二):软阴影
软阴影是通过对阴影图进行多次采样实现的
因为多个片段经常会对应同一个阴影纹理像素,单次采样会产生严重的锯齿问题
另外软阴影还可以产生比较柔和的边界,看起来比较自然
锯齿产生的原因
仔细思考阴影锯齿产生的原因,可以想到多个片段对应同一个阴影像素时,其在该阴影像素中对应的的位置是不同的
如下示意图,黑色的大方格表示单个阴影纹理像素,虚线分割的部分表示对应到该阴影像素的9个片段
显然对于AB来说其阴影值和C应该是不同的,算法中应该把这个因素考虑进去
可以想到,当纹理坐标没有对应到正中心时,通过与相邻像素的线性插值也许可以很好的解决我们遇到的锯齿问题
2×2线性插值
如上图,实线分割的四个区域表示阴影纹理中的四个像素,ABCD为其中心点
当一个片段映射到ABCD虚线包含的区域时,这四个像素都应该参与阴影计算,然后通过线性插值得出最终结果
假设纹理大小为float2 size(w, h),像素大小texelSize=float2(1/w, 1/h),当前纹理坐标uv
则以下代码可计算四个点纹理坐标uva/uvb/uvc/uvd,插值系数factor(x, y)【 水平方向插值系数为x, 垂直方向插值为y】:
factor= frac(uv*size-0.5);
uva = (floor(uv*size-0.5) + 0.5) * texelSize; uvb = uva + float(1, 0) * texelSize; uvc = uva + float(0, 1) * texelSize; uvd = uva + float(1, 1) * texelSize;
a、b、c、d分别为A、B、C、D阴影值
//a=depthShadow(sm,uva, z,...); b=depthShadow(uvb,...)
ab = lerp(a, b, factor.x);
cd = lerp(c, d, factor.x);
shdow = lerp(ab, cd, factor.y); //最终阴影值
这样即便单个阴影像素点也可以产生平滑的阴影,并具有典型的线性插值特征:
更大的采样范围
2x2线性插值虽然可以产生平滑过渡的阴影,但没有完全解决锯齿纹理,效果并不理想
这是因为使用的阴影图本身就是栅格化的
处理这个问题的办法是采用更大的采样范围比如3x3\4x4\5x5
其处理思路都是一样的,比如以5x5为例:
首先计算各行的阴影值: row[i]= b+c+d+lerp(a, e, factor.x) , 其中a b c d e为同一行的五个点阴影结果
总阴影值 = ( row1+row2+row3 + lerp(row0, row4, factor.y) ) / 16
5X5采样得到可以产生非常柔和的阴影
不过需要25次纹理采样,性能相比3×3的9次或 4x4的16次要差些