以上几个参考文章写的非常好.
我只是做了最第一篇的简单摘录. 作为我入门的学习.
阴影的实现
在光线追踪算法中, 实现阴影(shadow ray)更加符合直觉.
在光栅化算法中,基于ShadowMap的实现更加常见.
阴影的"软硬"
理想中的点光源会造成只有本影区的硬阴影(hard shadows)
但是现实中的光源毕竟本身有体积,会形成拥有半影区的软阴影(soft shadows)
两者的关系不是简单地将硬阴影的边缘模糊化处理就能得到软阴影
Shadow Map 思路
由于光栅化的渲染管线相比基于光线追踪的实现方式缺少全局性信息,
每个 fragment 并不清楚全局的光照情况,无法直接判断自己是否处于阴影中,
因此需要额外预渲染阶段。
-
第一次渲染中 以光源位置作为视点,
基于 Z-buffering 算法,将每个像素点的深度值(z-depth)也就是距离光源最近的对象距离记录在 Z-buffer 中,输出到 FBO(Framebuffer Object) 关联的 texture 中,生成 Shadow Map。 -
第二次场景渲染时,以正常摄像机作为视点,
将每个 fragment 到光源的距离和 Shadow Map 中保存的深度值进行比较,如果大于后者则说明被其他物体遮挡处于阴影之中。
整体思路就是这样,针对不同的光源类型有一些额外的注意点,比如:
- 平行光(Directional Light)并不存在确定的光源位置,在投影矩阵的选择上应该采用平行投影而非透视投影
- 平行光和聚光灯(Spot Light)都有固定的方向,而泛光灯(omnidirectional shadow maps)向四面八方发光。其实思路都是一致的,只是具体使用 Cubemap 保存 Shadow Map,可以参考 learnopengl.com - Point-Shadows
精度问题
- 当物体到光源的距离过远时,使用 RGBA 中任何一个分量存储深度值都会存在精度丢失问题,毕竟只有 1 byte
- 合适的做法是在 Shadow Fragment Shader 中充分利用四个分量也就是 4 bytes 存储
深度偏移问题
- 原因是 Shadow Map 的分辨率是离散的,多个 fragment 会对应到同一个纹素,
- 增加 Shadow Map 的分辨率只能减少可能性,并不能完全避免这个问题。
常用的解决办法是
- 给 Shadow map 中保存的深度值增加一个偏移值(Depth bias)
- 就是说在一定的深度范围偏移下,不认为是阴影.
但是 偏移值多大会造成“Peter Pan”现象(物体似乎飘在了空中),也叫漏光(light leaking)
所以这个偏移值的选择十分重要,如果采用一个固定值例如 0.005. 当表面法线与光源方向夹角很大(上图中表面更加倾斜)时还是会出现
更好的做法是根据法线方向和光线方向计算:
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
走样问题
- 锯齿状的痕迹也被称作走样(Aliasing)
- 还是由于 Shadow Map是离散的.分辨率有限. 多个fragment对应同一个纹素了.
解决方法: 反走样 抗锯齿
PCF (Percentage Closer Filtering)
- 最近邻采样
- 双线性插值
- Poisson Disk
- Stratified Poisson Disk
- Rotated Poisson Disk