【Graphic】阴影


阴影映射(Shadow Mapping)

阴影映射的原理很简单,我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。

这里所有蓝线代表光源可以看到的fragment。黑线代表被遮挡的fragment,即渲染为阴影。

在深度测试中,储存在深度缓冲里的一个值是摄像机视角下的对应于一个片段0-1之间的深度值。
如果我们从光源的透视图来渲染场景,并把深度值的结果储存到纹理中,通过这种方式,我们就能对光源的透视图所见的最近深度值进行采样。最终,深度值就会显示从光源的透视图下见到的第一个片段了。我们管储存在纹理中的所有这些深度值,叫做深度贴图(depth map)或阴影贴图

这张图可以很好地帮助理解。我们通过矩阵T来获得每个点在光照空间的坐标。如左图,P点是摄像机可以看到的点,因此需要通过T(P)转化到光照空间中,从而获得深度值0.9。而C点摄像机捕捉不到,但是可以直接在光照空间中获得深度值0.4。通过比较两个点深度值,我们判断出P点处在阴影当中。

主要流程

  1. 渲染深度贴图。
    • 建立一个texture和FBO帧缓冲用来保存深度值信息。由于只关心深度值,我们要把纹理格式指定为GL_DEPTH_COMPONENT。
    • 计算光照空间矩阵:由于是平行投影,所以设置投影矩阵为正交投影矩阵(记得要先从模型空间变换到世界空间)。
    • 进行渲染
  2. 使用生成的深度贴图来计算片段是否在阴影之中。
    • Vertex Shader:传递一个普通的经变换的世界空间顶点位置vs_out.FragPos和一个光空间的vs_out.FragPosLightSpace给片段着色器。
    • Fragment Shader:需要计算一个shadow值。1.先将光线空间坐标xyz除以w可求出当前坐标的深度值[-1,1]。2. 将当前光线空间的坐标的深度值深度贴图中的深度值(光的位置视野下最近的深度)作比较,如果currentDepth > closetDepth,那么片段就在阴影中。

阴影失真(Shadow Acne)

可以看到地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做阴影失真(Shadow Acne)

因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的同一个值中去采样。这就会导致阴影失真。如图:以四个格子为四个fragment为例,由于深度图分辨率有限四个格子会采样同一个深度值。
如图假设采样这个四方格的正中心,假设这个深度值为10。

这四个fragment分别取名a,b,c,d,由于光源的位置会导致求得的四个距离会不一样,先求a到光源的距离假设为9.8<10,b到光源的距离是11.6 >10,c到光源的距离12.1>10,c到光源的距离9.5<10,。就会导致a和d亮,b和c暗,他们本来应该都是亮的才对。然后整个光源视椎体下各个片段都会出现这个问题,就会出现上面的明暗交错的条纹。

阴影偏移(shadow bias)

我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了。我们可以根据表面朝向光线的角度更改偏移量
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

悬浮(Peter Panning)

使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,可以从下图看到这个现象:

如果是有”厚度”或者”封闭”的模型, 在渲染DepthMap时可以画它们的背面, 这样在进行Depth比较的时候, 就不会出现太多相近的Depth值了. 不过像上图的单层地面(或者常见的TerrainMesh), 这个方法是没法用的.

PCF(percentage-closer filtering)

因为深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。

可以通过增加深度贴图的分辨率的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。

PCF的核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。

点光源阴影

对于深度贴图,我们需要从一个点光源的所有渲染场景,普通2D深度贴图不能工作;如果我们使用立方体贴图会怎样?因为立方体贴图可以储存6个面的环境数据,它可以将整个场景渲染到立方体贴图的每个面上,把它们当作点光源四周的深度值来采样。

生成后的深度立方体贴图被传递到光照像素着色器,它会用一个方向向量来采样立方体贴图,从而得到当前的fragment的深度(从光的透视图)。大部分复杂的事情已经在阴影映射教程中讨论过了。算法只是在深度立方体贴图生成上稍微复杂一点。

CSM

在平行光阴影的渲染中,会出现一些问题导致质量以及性能的下降。

  • 平行光需要靠近摄像机视锥体中心,让其生成的阴影贴图不会离摄像机观察的空间过远导致采样越界;
  • 在大场景中既需要保证阴影质量,又需要覆盖面广,从而使得目力所及的地方都能够渲染阴影;
  • 离摄像机较远的物体不用采样和近处分辨率一样高的阴影贴图,以节省性能。
  • 相机近端和远端物体对阴影图采样精度一样,会导致近端物体采样精度不够,远端物体采样精度浪费。

级联阴影映射(CSM)就是基于解决上面的问题所开发出的技术。
主要思想是:通过将摄像机视锥体按照远近切分为几个部分,同时对每一部分都调整灯光位置和灯光视锥体范围渲染一张分辨率相同的阴影贴图,灯光视锥体覆盖范围依照远近由小到大;在物体采样时也根据其处于视锥体的哪个部分(即离摄像机的远近)来采样对应的阴影贴图。

CSM在每帧所执行的步骤为:

  1. 将视锥体划分为子视锥体块
  2. 计算每个子视锥体的正交投影(光源空间中正交投影)
  3. 为每个子视锥体渲染一个阴影贴图
  4. 渲染场景
    a. 绑定阴影贴图并渲染
    b. Vertex Shader执行以下操作:计算每个光子视锥体的纹理坐标;转换灯光顶点
    c. Fragment Shader执行以下操作:确定合适的阴影贴图;如有必要,转换纹理坐标;对级联进行采样;给像素点着色。

视锥体的划分

在实践中,重新计算每帧的视锥分裂会导致阴影边缘闪烁。普遍接受的做法是在每个场景中使用一组静态的级联间隔。

当场景的 Z 范围非常大时,需要更多的分割平面。当大部分几何体聚集在视锥体的一小部分时,需要较少的级联

光源空间中正交投影

光视锥体,如第一个图中倾斜的矩形,这一步就来计算他
光视锥体就像一个包围盒。但是,仅仅包裹分割后的摄像机视锥体是不够的。如果在光源和分割后的摄像机视锥体之间存在可以投射阴影的遮挡物,我们应该扩展光视锥体的大小,将遮挡物包裹在光视锥体中。

  1. 先计算出摄像机视锥体分块在世界空间中的坐标
  2. 利用分块的各顶点坐标,计算摄像机视锥体分段的“包围盒”,从而计算出分块对应的正交投影矩阵。
  3. 计算出摄像机视锥体分块的远平面在摄像机空间中投影后的位置,并把他变换到[0.0,1.0]。这么做的目的是为了在片段着色器中判断某个片段属于哪个分块

渲染阴影贴图

通过上面的铺垫,就可以渲染出不同分段的阴影贴图,并在最后的片段着色器中判断该使用哪个阴影贴图,最后渲染出阴影

物体阴影采样

通过上述步骤,我们得到了一个 Texture Array,内部存有所有分块的阴影贴图。在渲染场景时,通过这个阴影贴图、分块参数和所有分块灯光的变换矩阵,根据片元深度计算需要采样的阴影贴图并结合着基础的阴影映射步骤进行采样即可。

当然关于阴影还有很多技术并没有提及,比如实时渲染中的软阴影等技术。

参考

阴影映射
关于Acne的回答
Cascaded Shadow Maps
级联阴影映射


文章作者: YukinoKyoU
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YukinoKyoU !
评论
  目录