阴影生成原理

生成阴影一般采用对场景进行两次渲染的方法,第一次是让摄像机从光源位置看向场景进行渲染,这次渲染只记录深度信息,生成ShadowMap,第二次是进行真正的渲染,在渲染过程中会将当前顶点转换到第一次渲染的光源空间,进行深度采样并与当前的深度进行对比,如果当前深度大于ShadowMap记录的深度,说明物体被遮挡,则产生阴影

阴影背后的数学原理

微积分中有如下不等式

考虑Shading Point会不会接受四面八方的光照时,渲染方程如下

即Wo方向的反射光合集是所有间接光经过BRDF反射到Wo方向的可见光

结合以上约等式和渲染方程可以得到

也就是说Visibility可以被单独拆开考虑,最后乘以Shading计算结果即可,和ShadowMap的思路是一致的

约等式成立的条件为:

  1. g(x)积分范围很小,也就是渲染方程中的dwi范围很小时,在点光源和方向光源中只有一个点和一个方向有光照是准确的
  2. 当g(x)平滑时,也就是渲染方程中Shading的结果是平滑的时,在Shading计算是diffuse(各个方向Shading一致)或者面光源(每个点视为相同点光源)是准确的

ShadowMap问题与解决方案

自遮挡问题与解决方案

由于ShadowMap中的一个像素点在实际覆盖的是场景中的一个区域,但是一个区域的深度不一定是相等的,即ShadowMap记录的离散值,实际场景是连续值,导致覆盖区域一部分实际深度大于ShadowMap深度,一部分小于ShadowMap深度,从而引发当前深度大于ShadowMap深度的判断出现问题,造成渲染结果会出现造成一半黑一半亮的交错条纹,尤其是在光源的方向与地面方向接近平行时现象最为明显

对于该问题,一种解决办法为增加一个Bias,当前深度 + Bias < ShadowMap采样深度时才判断为遮挡,这样可以解决这个问题,但是Bias会导致阴影和物体连接位置的阴影消失,因此一般工业界会通过调整Bia的大小让阴影尽可能做到既不自遮挡也不会丢失Shadow

在学术界,该问题通过存储两个深度值来解决,既要存储最小深度也要存储次小深度,然后通过中间值进行深度判断,但这要求模型都要有正反面不能是纸片,同时增加了计算量,工业界追求更高的效率并不使用

走样问题与解决方案

同样由于ShadowMap是离散的,根据ShadowMap判断的阴影容易产生走样,精度越低走样越明显

对于该问题,一个解决方案是联级阴影贴图,将渲染ShadowMap的视椎体切成多个,不同的深度区间有生成不同的ShadowMap来提高精度

此外还有通过PCF进行阴影模糊化,降低锯齿感

软阴影

在上述所述的阴影计算方法中,我们最终得到的阴影效果是硬阴影,边缘是非0即1的锐利阴影,我们日常生活中看到的绝大多数光源都有体积,产生的阴影是软阴影,存在明显的过渡

PCF

PCF最初用于抗锯齿,后来被应用于软阴影其主要算法是将原来当前深度与ShadowMap一个点进行01判断转化为与ShadowMap的一堆点进行01判断并去平均,注意这里的求平均不是将ShadowMap进行求平均也不是将渲染结果进行求平均,是以该点在ShadowMap对应的点为中心的一圈点进行判断后取平均,将原来的结果从0/1变成可能是0.6这样的值,从而降低锯齿感

PCSS

PCSS是基于PCF实现软阴影效果,在PCF中,我们有一个FileSize来定义ShadowMap中取的区域的大小,而PCSS通过动态调整的FileSize的方法来实现投射的近的阴影偏硬,投射的远的阴影偏软的更接近实际的软阴影效果,我们会根据Penumbra的大小来调整FileSize,根据相似三角形得到以下公式

其中Wlight和dReceiver是可以拿到的,对于dBlocker即光源与遮挡物的距离,我们需要对ShadowMap进行采样,再对一个区域的遮挡深度取平均,对于这个区域的大小即BlockerSize,我们可以取一个定值也可以通过将接受物连线光源,将ShadowMap置于近裁面,我们根据相似三角形得到

BlockSize = (dReceiver - NearClipDis) * LightSize / dReceiver ,即BlockSize随着距离和LightSize的增大而增大(这一点原视频貌似说反了)