【以前的文章存档】
Cesium的阴影技术用的是Cascaded Shadow Mapping(简称CSM),
先来说下两种阴影类型,
一种是硬阴影
基本光线跟踪阴影:
- 从相机发出射线
- 射线交于物体表面
- 发射阴影射线检查光线是否到达这一点
- 如果阴影射线被阻挡,那么该点就处在阴影中
另一种是软阴影
- 比硬阴影更真实
- 光源作为物理对象
- 阴影射线现在更像锥体
- Penumbra:部分阴影
- Umbra:全部阴影
常用的阴影技术
体积阴影(Shadow Volumes)
- 阴影用空间中的多边形体来表示
- 优点:准确的硬阴影
- 缺点:速度慢,需要大量的光栅化
基本方法:
- 对于场景中的每个三角形,将其边缘投射到沿着光的方向无限延伸的地方
- 这个截去头部的金字塔就是阴影体
- 位于阴影体中的场景的任何部分都属于阴影
实现方法:
- 深度预先计算: 将场景渲染到深度缓冲中。
- 渲染所有的阴影体
- 如果阴影体片段通过深度测试:
- 如果三角形是正面的,增加模板缓冲
- 如果三角形是背面的,减少模板缓冲
- 如果模板缓冲区在特定像素处的值为0,则该像素不处在阴影中
- 如果阴影体片段通过深度测试:
- 现在再次渲染场景,启用颜色缓冲
- 使用模板缓冲区作为掩码。如果模板缓冲对于特定的片段值大于0,则丢弃该片段
- 否则,像往常一样计算漫反射和高光照明
阴影贴图(Shadow Maps)
- 阴影是通过检测深度缓冲来确定的
- 优点:速度快,支持软阴影
- 缺点:占用内存高,有锯齿
基本方法:
- 从光源的角度渲染这个场景
- 把光源当作照相机
- 渲染出一个深度纹理来创建阴影贴图
- 从照相机的角度渲染这个场景
- 在顶点着色器中将每个顶点从世界空间坐标转换为光源空间坐标。
- 把光源空间位置传送到片元着色器。
- 将片元的深度与存储在阴影贴图中的深度进行比较。如果深度较大,则被阴影覆盖。
光源类型
- 平行光(太阳)————使用正交投影
- 聚光灯————使用透视投影
- 点光源————类似于聚光灯,但是需要一个全向阴影贴图
- 创建六个光视锥,并在几何着色器中渲染到立方体贴图(cube map)。
- 阴影贴图是昂贵的;在一个标准的视频游戏中通常只有一个灯源投射阴影。
阴影贴图存在的问题
- 投影走样(Projective Aliasing)
- 透视走样(Perspective Aliasing)
- 纹理分辨率限制(Texture Resolution Limits)
投影走样
- 当几何图形的斜率平行于光的方向时候发生
- 最好的情况: 头顶光源,平坦的地板
- 最坏的情况: 头顶光源,竖直的墙壁
- 即使在最好的情况下,深度缓冲的精度,阴影贴图的分辨率和浮点数比较也会导致该问题。
- 深度冲突(z-fighting)
- 深度冲突(z-fighting)
- 会严重影响圆的边缘地方
深度偏移
- 应用一个恒定的深度偏移
- 在光通过时,把深度稍微往深推一点
- 现在在最好的情况下深度比较测试会成功
- 但是在最坏的情况下仍然有问题
- 深度偏差过小会导致z-fighting
- 深度偏差过大会导致光线泄漏
- 偏差大小应该取决于三角形的斜率
- 使用由硬件计算出来的屏幕空间导数
- glPolygonOffset利用屏幕空间导数自动计算偏移量
- 取一个常量参数和一个斜率缩放参数
- 仍然需要调整特定场景的参数
- 使用GLSL命令dFdx和dFdy将屏幕空间相邻像素转换为光空间斜率
- 更复杂,计算成本更高
- glPolygonOffset利用屏幕空间导数自动计算偏移量
其他实现
- 增加深度缓冲的精度
- GL_DEPTH_COMPONENT16
- GL_DEPTH_COMPONENT24
- GL_DEPTH_COMPONENT32F
- 选用光视锥合适的近平面和远平面,来适应场景
- 如果可能的话,增加阴影贴图的分辨率
- 线性化深度缓冲
- 相机可能朝向距离光源很远的一部分场景,我们想要看到和靠近灯光时一样的细节。
法线偏移
- 向法线方向推进深度
- 补充恒定深度偏差
法线着色
- 应用lambert shading可以隐藏走样
- 此外,适当的阴影看起来更真实。这是日落的照片
纹理分辨率
- 画面质量取决于纹理分辨率
- 低分辨率阴影贴图产生块状的结果
- 高分辨率的阴影贴图看起来更好,但是会占用大量内存
- 如果场景真的很大怎么办?
- 一种单一纹理不可能延伸到整个世界
- 一种单一纹理不可能延伸到整个世界
透视走样
- 视图空间中的像素大小不匹配阴影贴图中的纹素大小
高级阴影贴图
- 修正透视走样
- 眼睛附近需要更多的细节,远离眼睛的地方可以用更少的细节。
- 要处理纹理分辨率限制
- 保持纹理分辨率不变,独立于场景大小
- 眼睛周围的中心阴影贴图
- 阴影贴图不应该覆盖视角外面的区域
- 解决方案:
- 扭曲技术
- 透视阴影贴图(PSM)
- 光源空间透视阴影贴图(LiSPSM)
- 对数透视阴影贴图(LogPSM)
- 视锥体分割技术
- 级联阴影贴图
- aka Z-partitioning,平行分割贴图
- 样本分布阴影贴图
- 级联阴影贴图
- 扭曲技术
透视阴影贴图
- 先将透视变换应用到场景中,然后再进行渲染阴影贴图
- 只需替换标准的视图-投影矩阵
- 倾斜阴影贴图,使靠近眼睛的区域有更多的密度
- 仍然使用相同分辨率的单个阴影贴图,但获取更多
光源空间透视阴影贴图
- 修正了透视阴影贴图的局限性
- 透视变换应用于光的视投影矩阵而不是眼睛的视投影矩阵
- 处理位于viewer后面的能投射阴影的物体
- 灯不改变他们的类型(PSM可能不能正确地把方向光转换成点光源)
- 总体上更稳定,误差分布更好
- 透视变换应用于光的视投影矩阵而不是眼睛的视投影矩阵
对数透视阴影贴图
- 透视投影+对数变换
- 最佳常数误差
- 需要对数光栅化,目前不支持当前的GPU硬件
结果
- 标准的阴影贴图
- 透视扭曲(LiPSM)
- 对数透视扭曲
级联阴影贴图
- 将光源视锥分割成多个视锥块
- 靠近眼睛的密度高,远离眼睛的密度低
- 每个子视锥体都有自己的阴影贴图。它们的尺寸都一样
- 片段着色器采样适当的阴影贴图
如何选择划分方式
- 基于特定视图选择静态分区
- 鸟瞰视图只需要分很少段
- 当场景延伸很远时标准行走视图需要多个级联视段
- 总是调整参数比较麻烦
- 分类型
- 自己决定
- uniform
- 对数
- 找中点
- 在Cesium里,倾向于用对数分割(0.9 lambda)
- 在某些情况下使用显式级联距离
- 照相机靠近一个很小的物体时
- 最好有一个动态的方法
- 样本分布阴影贴图
- 使用几何信息和遮挡测试创建紧密绑定的frusta
- 近似对数分裂
结果
拟合的阴影贴图
- 从深度缓冲区中获取深度范围是理想的,但代价昂贵
- 下一个最好的方法是从包围盒里获取近/远平面
- 深度范围最初为10,000米
- 实际深度范围小于500米
- 适用于自顶向下视图,地平线视图没有区别
平滑硬阴影
- 线性过滤
- 百分比渐进过滤
- 方差阴影贴图
线性过滤
- 启用阴影贴图纹理的线性过滤
- 插值2x2像素区域的深度而不是仅仅选择最近像素的深度
- 真的很简单。但并不完全正确。
百分比渐进过滤
- 通过观察邻近阴影纹素来模拟软阴影
- 取阴影贴图中4个邻近样本点
- 使用GLSL命令textureGather
- 使用GLSL命令textureGather
- 比较每一个样本点的表面的深度
- 为textureGather调用提供表面深度
- 双线性插值结果
- 不同于线性过滤,它是对深度值进行插值,而不是结果比较
- 为了得到更大的半影,调用4个textureGather命令来采样16个邻近点
方差阴影贴图
- 在一张贴图中存储深度,在另一张贴图中存储深度的平方
- 根据你的喜好过滤这些贴图
- Mip-map
- 高斯模糊
- 区域求和表
- 通过一个概率函数确定片元的阴影强度
- 优点:
- 能够用比PCF小得多的成本来捕捉大的半影
实现细节(Cesium)
需要妥协的地方
- 精度/效率
- 当工作在不同的3D模型、相机视角下,需要选择合适的bias值