1.基本问题和相关
Common Techniques to Improve Shadow Depth Maps:
https://msdn.microsoft.com/en-us/library/windows/desktop/ee416324(v=vs.85).aspx
Cascaded Shadow Maps
https://msdn.microsoft.com/en-us/library/windows/desktop/ee416307(v=vs.85).aspx
Soft shadow
PCSS: http://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf
PCSS shader sample: http://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf
Translucent shadow
http://www.crytek.com/download/Playing%20with%20Real-Time%20Shadows.pdf
Shadows & Transparency
Translucency map generation:
- Depth testing using depth buffer from a regular opaque shadow map to avoid back projection/leaking
- Transparency alpha is accumulated only for objects that are not in “opaque” shadows
- Alpha blended shadow generation pass to accumulate translucency alpha (sorted back to front)
- In case of cascaded shadow maps, generate translucency map for each cascade
- Shadow terms from shadow map and translucency map are both combined during deferred shadow passes with max() operation S
2.实现细节和问题
Hardware shadow map:
D16/ D32 / D24S8 作为rendertarget.
Hardware PCF:
- 纹理采样开启双线性过滤
- d3d9用tex2Dproj, d3d11用SampleCmpLevelZero
最小视锥(minimal frustum)
在shadow map贴图大小固定的情况下, 视锥越小, shadow map上的内容越少(有效内容不变), 所以利用率和分辨率越高.
naive 实现:
1.计算场景包围盒
2.根据场景包围盒计算最小视锥
3.渲染shadow map
更好的实现:
1.计算场景包围盒
2.用场景相机裁剪这个包围盒
3.用裁剪后的凸包体,计算最小视锥
4.渲染shadow map
由于场景包围盒被相机裁剪(求交), 所以包围盒变小, 那么生成的视锥也变得更小.
因为视锥是不规则六面体, 视锥和AABB求交得到的是一个凸包.求交方法可以看Ogre, Ogre有convex实现.
我用的另外一个方法: 用aabb 的12条边和视锥的6个面求交, 视锥的12个边和aabb的6个面求交, 得到的交点如果同时在aabb和视锥内就是凸包体的顶点.
如果要计算凸包在Light space的包围盒, 那么应该先将每个顶点变换到light space, 再求包围盒.
如果先求包围盒, 在把包围盒变换到light space, 因为AABB的轴对齐特性, 变换以后通常会变大.
Depth bias based on frustum depth range
因为使用的是Hardware shadowmap, 所以需要指定depth bias和slope scaled bias.
由于frustum根据根据convex的顶点计算来. 如果frustum的深度范围(znear, zfar)不固定的话, 那么相同的bias值, 对应的误差会有浮动.
所以可以根据视锥的深度范围来计算bias, 这样的误差值是固定的. 方法:
渲染shadow map depth的时候只指定slope scaled bias, 不指定depth bias.
渲染物体的时候, shadow matrix = M * light_projection * light_view , 也就是再乘以一个参数矩阵 M (DepthBias):
float offset = isD3D9 ? 0.5f / (float)ShadowMapSize : 0f; //half texel offset float bias = -0.0015f / (DepthRange) * adjustScale; DepthBias[0] = Vector4(1, 0.0f, 0.0f, offset)); DepthBias[1] = Vector4(0.0f, 1, 0.0f, offset)); DepthBias[2] = Vector4(0.0f, 0.0f, 1.0f, bias)); DepthBias[3] = Vector4(0.0f, 0.0f, 0.0f, 1.0f));
同时由于计算出的uv要采样shadowmap, 对于dx9来说, 同时可以预先应用half pixel offset.
PCSS:
PCSS的原理比较简单, 也有很多变种. 目前用的是标准的实现. 遇到的问题:
PCSS的半影采样范围是根据相似三角形计算出来的
float PenumbraSize(float zReceiver, float zBlocker) //Parallel plane estimation { return (zReceiver - zBlocker) / zBlocker; }
如果zblocker的深度太小(接近0), 那么半影采样范围就变得非常大, 难以接受.
这种情况在做Self shadow的时候会遇到, 因为视锥是很小的. 解决办法:
1.用户指定半影的大小范围(shader constant, lightWidthMin, lightWidthMax), 然后根据深度(距离)做线性插值.
或者 2.放大depth range, 这样最小的depth也不会接近0
如果产生阴影的物体和接受阴影的物体靠得太近, zReceiver - zBlocker 太小, 导致半影范围接近于0, 导致锯齿:
解决办法可以用上面的线性插值, 因为线性插值最小值是lightWidthMin, 保证有最小的半影范围.
或者: 在半影范围上加一个常量值, 比如1.0/shadowMapSize
个人使用lightwidth_min和max线性插值, 这样也方便美术调控参数.
Translucent shadow(not alpha test):
上面已经贴出的Crytek和StarCraftII的方法了, 方式比较类似.
我这里的简单实现:
R8 + Depth16, alpha和depth同时绘制, 一个color buffer保存alpha, 一个depth buffer保存深度.
opaque: Depth test - less, disable color write.
transparency: Depth test - less, disable Z write, output alpha, enable color blending(addative)
这种方式比较简单, 一次绘制没有render target切换, 先画不透明物体再画半透明物体. shader中对于alpha的阴影判断也比较hacky: 如果alpha值(R8.color.r)不为0, 则认为有阴影, 不需要比较深度. 因为能写alpha值的时候, 说明深度测试less通过了. 对于PCSS需要深度的, 可以模拟一个深度值.
问题: 不支持自阴影(doesn‘t support self shadow). 产生阴影的半透明物体本身, 如果要计算阴影, 根据alpha!=0这个判断, 也是有阴影的...
改进方式:
基于上面的方式, 给transparent objects再加上一个depth pass, 绘制阴影时采样两张深度图. 或者将前面的R8改为RGBA, A保存透明度, RGB打包深度, 单独混合alpha通道, 这样不用切换render target.
两个depth pass的话多一张D16的贴图, 显存占用要比RGBA小.
问题: 自阴影错误
| | |
a1 a2 O
如上, O是不透明物体, a1和a2是透明物体.
当出现多层透明物体的时候, O的阴影是对的, 因为a1和a2的alpha 会混合.
基于上面的改进, 因为有了深度信息, 再加上bias, 所以a1不会有阴影, 也是对的.
但是a2的阴影和O的阴影是一样的, 都是基于同一个alpha计算出来的.由于a2是半透明物体, 阴影表现没有那么明显, 这里的问题可以忽略.
或者: 用深度来做线性插值进行:
shadow(uv.xyz) = lerp(1-alpha_a1a2, 1, saturate( (depth_O - uv.z) / (deoth_O - depth_a1) ) );
其中uv.z是shader中当前物体的深度. alpha_a1a2是alpha混合的结果. depth_O是opaque shadow map采样出的深度. depth_a1是transparent shadow map采样出来的深度.
这其实还是不对的, 因为a2的阴影透明度是1-alpha_a1, 跟距离无关, 但是可以解决透明物体和非透明物体靠的太近时的z fighting和shadow acne.
上面是工作中主要遇到的问题. 另外简单记录一下其他东西.
CSM
如果所有阴影都产生在一张shadowmap上,那么近处的分辨率也会比较低.CSM主要是通过多个cascade来提高近处的阴影分辨率.
之前工作中也做过PSSM, 一般会把多个shadowmap合到一张上, 比如4张1024x1024的shadowmap, 可以用2048x2048的贴图, 通过viewport来绘制四个区域. 上面Crytek也提到了.
还有CSM边界分割处也需要blend处理, 不然会有缝隙.
PSM
perspect最大的特点是近大远小, 所以使用pserspective 投影, 来提高近处阴影的分辨率.
但一般方向光都是平行光, 需要用orthographic (parallel)投影, 但由于一般场景相机都是perspective, 视锥不是box, 但在投影以后(post perspective space)是一个box, D3D是z[0,1]的扁盒子, OGL是一个cube. 在这个空间下, 因为方向会有切变, 所以原本世界空间的方向光到了post perspective space就变成了点光源, 可以用perspective projection了.
原理大致是这样, 实现的话会比较tricky.
另外一个Light space perspective shadow map (LiSPSM), 也是一种PSM, 还是利用perspective 投影来提高近处投影的精度. 主要的改变是不在用场景相机的post perspective space. 因为perspective投影下大部分方向都会有形变, 但是垂直于视方向(平行于xy平面)的方向不会有改变. LiSPSM就是利用这一点, 使用一个垂直于光照方向的透视投影, 来渲染深度. 如果直接用这个视锥渲染的话, y值就是沿着光照方向的深度值. 所以先把光空间变换到垂直于光照方向, 在变换回来, 得到的z就是深度了. 这个实现上要比PSM简单得多. 我也尝试了一下, 但是和一般的shadow map差别不大, 可能是实现上有点问题, 或者是只渲染了一个角色的自阴影的问题.