基于屏幕空间的实时全局光照(Real-time Global Illumination Based On Screen Space)

所谓基于屏幕,就是指利用的信息来源于“屏幕”,例如:frame buffer、depth buffer、G-buffer 都记录着屏幕所看到的各 pixel 的信息。

Reflective Shadow Maps(RSM)

Reflective Shadow Maps(RSM):主要是利用了类似 shadow map 思想的GI技术,但 shadow map 严格意义上不属于用户的“屏幕”信息,而是属于光源的“屏幕”信息,为了懒得再写多一篇博客分类,我还是将其归纳为 screen space 的技术。

RSM的思路:将受到直接光照的地方都视为次级光源,那么 shading point x 所受的次级光照便是来源于各个次级光源的反射。

次级光照 = bounce 为1的间接光照,RSM算法只能支持 bounce 为 1 的间接光照效果

然后,假定次级光源均是 diffuse 物体,那么一小块次级光源 patch(这块次级光源面积位于点 \(x_p\) )对 shading point x 的 irradiance 贡献是:

\[E_{p}(x) = \Phi_p \frac{\max (\mathbf{n_p} \cdot normalize(x-x_p),0)\max (\mathbf{n} \cdot normalize(x_{p}-x),0)}{\left\|x-x_{p}\right\|^{2}} \]

\(\Phi\) 是次级光源 patch 的 radiant flux,\(\mathbf{n_p}\)\(x_p\) 的法线 ,\(\mathbf{n}\)\(x\) 的法线

所有的次级光源 patch 对 \(x\) 的贡献加起来便是 \(x\) 的间接光照 irradiance:

\[E(x) =\sum \ E_{p}(x) \]

那么,怎么找到这些次级光源呢?这就用到了 shadow map 的思想:

  1. 阴影生成 pass:在光源摄像机渲染 shadow map (往往只记录了深度)的时候,顺便额外记录 世界坐标 \(x_p\) 、法线 \(n_p\)、 接受的直接光源 radiant flux \(\Phi_p\)。那么就可以认为 shadow map 的一个 texel 对应一块patch ,从而这张 shadow map 就包含了所有次级光照 patch 的信息了 。

实际上,世界坐标也可以通过uv坐标、遮挡深度来推算得到,好处是可以节省空间,坏处那自然是没那么精确了。

此外,计算一个 texel (或者说一块patch)的 \(\Phi_p\) 时,无论光源是directional light还是spot light,都不必计算 cosine 或者距离衰减,而直接用光源强度与物体 albedo 相乘

\[\Phi_p=\Phi(u_p,v_{p})= I * c_p \]

\(u_p、v_p\)\(x_p\) 在 shadow map 上的纹理坐标。

  1. 主渲染pass:在 pixel shader 阶段,计算出 \(x\) 对应的 shadow map uv坐标,并取该坐标周围若干个 texel (这些正是我们要采样的次级光源点)对应的 世界坐标 \(x_p\) 、法线 \(n_p\)、 接受的直接光源radiant flux \(\Phi_p\) ,它们将对 \(x\) 的渲染造成间接光照影响:

\[L_{indirect}(x,\mathbf{v}) = \frac{E(x)}{\pi} = \frac{{\sum_{\text {texels p}} \ E_{p}(x)}}{\pi} \]

RSM效果图:

RSM 重要性采样

理论上,为了实现最好的RSM效果,应当取整张 shadow map 的所有 texel 作为次级光源点,因为整张shadow map 意味着包含了整个光源照到的信息。但这样所需的采样数就相当于 shadow map 的分辨率,代价太高。

因此我们应当使用少量的采样数来保证性能,同时也要保证RSM的间接光源质量能够接受,那么就容易想到用 Importance Sampling 来加速采样的收敛。那么哪些地方的次级光源点比较重要呢?

RSM 假定,离 shading point x 近的点更可能给 x 的光照贡献大,而远的点给 x 的光照贡献小。

因此这个用于RSM的 Importance Sampling 将给近的的地方更多的采样点(当然权重更小),远的地方更少的采样点(权重更大),用可视化采样点数量和权重大概就是这个样子:

因此,选取一个随机采样点坐标 \((u,v)\) 和对应的权重 \(importance\)

\[(u,v)=\left(s+r_{\max } \xi_{1} \sin \left(2 \pi \xi_{2}\right), t+r_{\max } \xi_{1} \cos \left(2 \pi \xi_{2}\right)\right) \]

\[importance = (\xi_{1})^2 \]

其中,\(s、t\) 为 shading point x 在 shadow map 的纹理坐标,\(\xi_{1}、\xi_{2}\) 为随机数

RSM 的应用与缺陷

缺陷:

  • 性能开销与灯光数量成正比,有点昂贵(意味着需要同样数量的 shadow map、在多张 shadow map 采样等...)
  • 由于 shadow map 记录的是光源摄像机屏幕上的表面几何信息,因此在计算 patch 对 shading point 的贡献时很难做到检查 visibility:
  • 仅支持 one-bounce 间接光照效果
  • RSM 假设次级光源面均是 diffuse 的,这会影响图像的正确性(当然大部分情况下还是可以接受的)

应用:

  • 作为廉价的GI方法,常被用于做单个重要光源的GI效果(例如手电筒)

Screen Space Ambient Occulsion(SSAO)

屏幕空间环境光遮蔽(Screen Space Ambient Occulusion,SSAO):是一类游戏工业界很常用且廉价的屏幕空间 GI 方法。

所谓环境光遮蔽(AO),就是某个 shading point 因为被其它几何表面所遮挡,从而降低了接受环境光的比例(这种遮蔽常常发生在凹处表面):

AO 的基本公式:

\[A(x) = 1-\frac{1}{\pi}\int{}V(x,w_i) cos\theta_i \mathrm{d}w_i \]

AO 往往只代表一个简单的光线入射遮挡比例,乘于它意味着需要把 shading point 当作 diffuse 的表面来看待(即与观察方向无关,出射到哪都是 \(\frac{1}{\pi}\) 的 irradiance)。

一种计算 AO 的经典方法就是通过蒙特卡洛 + ray casting 去预计算一个模型上各处的 AO,然后做成该模型的 AO texture 后就可以在运行时采样并与环境光照值相乘(AO texture 存的是 visibility 值)。

而 SSAO 不需要预计算过程,而只需要通过屏幕的 depth 信息就能做到还算不错的 AO 效果。

SSAO 的算法流程:对于某个 shading point ,

img

  1. 在该点一定半径内的球型范围内随机采样 N 个点,然后这些采样点将与 depth buffer 对应的深度作比较:若采样点的深度小于 depth buffer 对应位置的深度,则说明该采样点被遮蔽了

  2. 根据所有被遮蔽得到采样点数量 \(Occ\),计算出 AO 为 \(A(p) = \frac{Occ}{N}\)

  3. 那么该 shading point 的环境光照即为

    \[L_{indirect}(x) = \frac{1-A(x)}{\pi} \int_{\Omega^{+}} L_{\mathrm{environment}}\left(\mathrm{x}, \omega_{i}\right) {\rho} \cos \theta_{i} \mathrm{~d} \omega_{i} \]

\(\rho\) 为 albedo。

SSAO 效果图(左为关闭SSAO效果,右为开启SSAO效果,可以看到物体交界处等地方多了更多的暗部细节):

SSAO Blur

实践中由于性能限制,SSAO 一般仅使用16个采样点,那么 AO 的结果将会是 noisy 的:

这时候就稍微修改下 SSAO 的算法流程,在计算 shading point 的 AO 时,不再直接乘于 color。而是先写入到一个 AO buffer 上,之后用一个屏幕后处理 pass 对 AO buffer 信息进行边缘保留滤波算法(其实就是保持边缘感的模糊操作,例如双边滤波算法),那么得到将是不那么 noisy 的 AO 结果:

SSAO 半球采样

实际上,渲染方程本就是上半球的积分,下半球的光线不会照到 shading point,因此 SSAO 采样范围不应该是一个球型,而应当是基于该点的法线为中心的半球形采样范围。

采样上半球采样范围的 SSAO 改进方法,得到该范围的采样点算法也很简单:

vec3 rand;				// 在球形上的随机坐标
vec3 n;					// shading point法线
rand = sign(dot(n,rand))*rand;	// 在半球上的随机坐标

img

SSAO 的应用与缺陷

缺陷:

  • 仅包含屏幕表面的几何信息不能表示完全正确的 visibility,因此 AO 效果不那么准确(相对于预计算AO贴图)

例如,下图中间点的采样,有个红色采样点实际上没有被遮蔽。但是该采样点的深度小于depth buffer的对应深度,因此被 SSAO 判定为遮蔽了。

  • 仅支持短距离的物体遮蔽效果

应用:

  • 廉价的GI效果,提升画面的暗部细节,大部分游戏都会将其纳入一种画面增强选项,虽然之后有性能和效果更好的 HBAO 算法作为取代。

Screen Space Directional Occlusion(SSDO)

Screen Space Directional Occlusion(SSDO) 也是一类与 SSAO 极其相似的屏幕空间 GI 方法,区别在于它们看待光线遮蔽的角度是相反的:

  • AO 认为 shading point 朝外的光线打到物体几何表面时,相当于外部环境光被这个表面遮挡了,因此(对于下面这幅图) AO 将红色部分视为间接光照来源,黄色部分视为损失的间接光照
  • 而 DO 认为 shading point 朝外的光线打到物体几何表面时,相当于受到了表面的间接光照,因此(对于下面这幅图) DO 会将黄色部分视为间接光照来源,红色部分视为损失的间接光照

也因此,SSAO 往往增加的是明暗细节,而 SSDO 往往增加的是周围表面的颜色影响(或者说增加 color bleeding 效果)。

SSDO 需要依赖屏幕的 color, depth 信息来完成。

SSDO 算法流程的思路也和 SSAO 相似:对于某个 shading point,

image-20221107015328926
  1. 先在 shading point 一定半径内的半球型范围内随机采样 N 个点,然后这些采样点将与 depth buffer 对应的深度作比较:若采样点的深度小于 depth buffer 对应位置的深度,则说明该采样点被遮蔽了
  2. 对于每个被遮蔽的采样点,将该采样点对应的 pixel 视为次级光源 patch,对 shading point 造成间接光照的贡献:

SSDO 计算该 GI 的时候会把物体几何表面所有 pixels(也包括 shading point 本身)都将假设为 diffuse 表面

\[E_p(x) = L_{direct}(p)(1-V_p(x)) \frac{{Area}(p)\cos\theta_{s_i}\cos\theta_{r_i}}{d^2_i} \]

\(Area(p)\) 即为 pixel p 的片元面积,可以通过 p 的深度算出(p越远,对应的片元面积越大)

  1. 累积所有被遮蔽采样点的间接光照,得到该 shading point 的间接光照 irradiance,并以 diffuse 形式反射到眼睛里:

\[E(x) = \sum_{i=0}^N E_{p_i}(x) \]

\[L_{indirect}(x) = \frac{\rho}{\pi} E(x) \]

SSDO 效果图:

SSDO 的应用与缺陷

缺陷:

  • 仅包含屏幕表面的几何信息仍然不能表示完全正确的 visibility,即会缺失屏幕看不到的平面信息(对于有颜色的GI效果很容易看出artifact)

  • 仅支持短距离GI效果,而无法展示长距离的GI
  • 仅支持 one-bounce 间接光照

Horizon Based Ambient Occlusion(HBAO)

前面 SSAO 计算 AO 的方式太过耗费性能,因为它是基于估量面积覆盖率去计算 AO 的(样本是三维空间分布的),而为什么我们不能基于方向角覆盖率去计算 AO (样本是二维空间分布)呢?

HBAO 就是基于方向角覆盖率的思想出发:在 shading point 上往各个方向进行 ray tracing(为了开销低,实际用的是ray marching),找到与 shading point 切平面夹角最大且 hit success(意味着遇到遮挡物)的 ray 方向,通过该 ray 方向和切平面的夹角 \(\alpha\) 就可以得到遮蔽率 :

\[A = \int^{\alpha}_{\phi=0}\cos(\phi)\mathrm{d}\phi = \sin(\alpha) \]

不过,直接计算 \(\alpha\) 是挺耗的,需要将 ray 向量和切向量点积后进行反三角函数,因此原论文采取了一种更容易计算的近似方式:将 \(\alpha\) 拆成两部分,一部分是 ray 方向与 view 平面的角度差 \(h\) ,另一部分是切平面与 view 平面的角度差 \(t\),其关系是 \(\alpha = h - t\)

在计算 ao cosine 贡献时,shading point 没有使用原来的 n 而是用 view 向量作为法向量,这样计算 AO 便可以:

\[A \approx \int^{h}_{\phi=t}\cos(\phi)\mathrm{d}\phi = \sin(h)-\sin(t) \]

image-20221108170008549

HBAO 的具体算法流程为:

  1. 对于 shading point p,我们先屏幕空间上采样四个方向,它们对应 \(\theta_1\),\(\theta_2\),\(\theta_3\),\(\theta_4\)
    • 初始的四个方向都是轴对齐的十字(虚线),然后对整个十字进行一个随机旋转角度,就可以得到当前帧的四个采样方向(实线)
image-20221109014226546
  1. 对于某个采样方向 \(\theta\)

    • shading point 上沿 \(\theta\) 方向进行 ray marching,最终找到可遇到遮蔽物与 view 平面最高仰角的 sin 值:

    \[tan(h(\theta)) = \frac{RayDir.z}{\sqrt{(RayDir.x)^2+(RayDir.y)^2}} \]

    \[\sin(h(\theta)) = \frac{\tan(h(\theta))}{\sqrt{1+\tan^2(h(\theta))}} \]

    如下图例子,找到的最高仰角为 \(S_3\) 方向上的。

    image-20221109013015134
    • 也顺便根据 ray tracing 结果对应的 hit dist \(r(\theta)\),计算出本次 \(\theta\) 角切面的样本权重为:

      \[W(\theta) = max(0,1-\frac{r(\theta)}{R_{max}}) \]

  • 根据 shading point 的 normal 算出其切线 tangent,算出切线与 view 平面的夹角的 sin 值,同理可算出 \(sin(t(\theta))\)
  1. 最终计算 AO 为:

\[\begin{aligned} A(x) & = \frac{1}{2\pi} \int_{\theta = -\pi}^{\pi} (\int^{h(\theta)}_{\phi=t(\theta)} \cos(\phi) \mathrm{d}\phi)W(\theta) \mathrm{d}\theta \\& = \frac{1}{2\pi} \int_{\theta = -\pi}^{\pi} ( \sin(h(\theta))-\sin(t(\theta)))W(\theta) \mathrm{d}\theta \end{aligned} \]

  1. 最后对 AO 图像进行一个空间滤波(或者说模糊)来降低噪声

image-20221119233953146

HBAO 的 Normal 问题

shading point 在利用 normal 计算切线 tangent 的时候,最好使用 face normal(即三角面法线)而非 interpolated normal(通过顶点插值得到的法线):如果使用 interpolated normal ,那么在模型凹角处容易出现 artifacts。

如下例,face normal 比起 interpolated normal 才更能做到 AO 的正确性:

image-20221109013148024

然而使用 face normal 后,对于低精度(low-tessellation)的曲面来说又会出现 artifacts:可能会出现黑白相间的现象,类似 shadow map 的 shadow acne 现象。

究其原因,如下图假设衔接处的 shading point 本应该是曲面,然而由于使用了低精度的 mesh,由 face normal 计算出来的 tangent plane 是偏下的,因此衔接处的 AO 过度偏暗。

这种情况反而用 interpolated normal 更能做到 AO 的正确性,不过由于大部分情况都不是曲面,因此还是建议算法以 face normal 为核心。

image-20221109013810882

那么干脆借鉴 shadow map bias 的做法,我们也给 HBAO 加 bias(例如可以让 tangent plane 往上抬高 30 度角来):

image-20221109013633893

HBAO 的采样

HBAO 在选择屏幕空间采样方向和在 ray marching 的时候可以使用开销极其低廉的预定义样本(可通过数组定义):每帧随机选择一种十字采样方向(共有5种),选定后每个采样方向 ray marching 也只步进两次。

image-20221118233031783

所以这种方式下 HBAO 的样本只需要 4个方向×2个步长 = 8个。其相比于 SSAO,样本数大大减少但却能同时能保持较好质量的 AO。

HBAO 的不连续问题

有些情况下,相邻的 shading point 可能 AO 是相差较大导致画面,如下图:

image-20221119212202268

其核心原因在于,\(P_0\) 本来 AO = 0.7,而 \(P_1\) 的遮挡物超出 ray marching 的最远距离,于是 AO 突变为 0。我们当然也可以采用更长距离的 ray marching,代价是要不牺牲性能采样更多样本,要不降低质量使用间隔较大的步长。

image-20221119213425968

为了减少这种不连续的情况,论文在 HBAO 的流程中引入了混合权重:

\[W(\theta) = max(0,1-\frac{r(\theta)}{R_{max}}) \]

也可以采用更平滑的混合权重:

\[W(\theta) = max(0,1-(\frac{r(\theta)}{R_{max}})^2) \]

虽然总体 AO 会减轻,但是却能够很好地解决不连续问题,并且不引入新的性能开销或者造成 AO 质量降低。

image-20221119233734008

HBAO 的应用与缺陷

应用:

  • HBAO 比 SSAO 开销往往更低:由于只需要更低于 SSAO 的样本数,甚至还能达到质量更高的 AO 效果。

缺陷:

  • 对于有中空的 AO 情况,HBAO 会把 hit success 的最高仰角以下的方向都认为是被遮挡的,这是与 Ground Truth AO 不符(SSAO 反而更能适应这种中空情形)。

如图左,hit success 的是橙色射线 ,hit fail 的是绿色射线,而图右的 HBAO 统统认为最高仰角以下的射线都是 hit fail。

image-20221120002230605

有空再补一补 HBAO+ 和 GTAO 的档

Screen Space Reflection(SSR)/Screen Space Ray Tracing(SSRT)

Screen Space Reflection(SSR),一类与 ray tracing 思路非常相似的屏幕空间GI方法,因此也有被叫为 Screen Space Ray Tracing(SSRT)

它的想法是,将屏幕所看到的表面几何信息当成一个场景,然后计算间接光照时,往半球范围若干个方向投射射线,看看能和这个场景的哪个屏幕像素点相交,这些便可以相交的像素点便是提供间接光照的来源。

SSR 需要用到的屏幕信息:color、normal、depth

SSR 的算法流程:

  1. 在第一个 pass 只渲染整个场景的直接光照,得到包含直接光照结果的 color buffer 、normal buffer、 depth buffer。

  2. 在第二个 pass 对整个屏幕渲染,对于某个 shading point ,在该点往半球随机方向投射若干条射线(使用 ray marching算法),然后将与射线相交的点 \(\mathrm{p'}\) 将对 shading point 的间接光照做出贡献(这与渲染方程是一致的):

    \[L_{\mathrm{indirect}}\left(\mathrm{p}, \omega_{o}\right) = \int_{\Omega^{+},V=1} L_{}\left(\mathrm{p'}, \omega_{i}\right) f_{r}\left(\mathrm{p}, \omega_{i}, \omega_{o}\right) \cos \theta_{i} \mathrm{~d} \omega_{i} \]

    其中当射线命中时, \(V = 1\) ;否则,\(V = 0\)

    为了减少计算,这里仍然假设次级光源点是 diffuse 的,这样式子实际可以写成:

    \[L_{\mathrm{indirect}}\left(\mathrm{p}, \omega_{o}\right) = \int_{\Omega^{+},V=1} \frac{E(\mathrm{p'})}{\pi} f_{r}\left(\mathrm{p}, \omega_{i}, \omega_{o}\right) \cos \theta_{i} \mathrm{~d} \omega_{i} \]

此外,SSR 还可以通过使用不同的 brdf 来实现不同的反射效果:

SSR 效果图:

SSR的 Ray Marching

得益于带 depth buffer,SSR 可以实现比较廉价的 Ray Marching 效果。Ray Marching 的精度和性能之间的平衡将取决于 march 的步长。

算法先从 start point 开始,

  1. 每次往射线方向走一个步长得到一个测试点,将该测试点变换成屏幕坐标 \((u,v,z)\)
  2. 根据uv坐标取 depth buffer 对应的深度 \(d\)\(z\) 比较:若 \(z>d\) ,则说明射线碰到该uv位置上像素点的“柱条”,返还该测试点;否则,重复上述步骤
bool RayMarch(vec3 ori, vec3 dir, out vec3 hitPos) {
  float step = 1.0;
  vec3 lastPoint = ori;
  for(int i=0;i<10;++i){
    // 往射线方向走一步得到测试点深度
    vec3 testPoint = lastPoint + step * dir;
    float testDepth = GetDepth(testPoint);
    // 测试点的uv位置对应在depth buffer的深度
    vec2 testScreenUV = GetScreenCoordinate(testPoint);
    float bufferDepth = GetGBufferDepth(testScreenUV);
    // 若测试点深度 > depth buffer深度,则说明光线相交于该测试点位置所在的像素柱条
    if(testDepth-bufferDepth > -1e-6){
      hitPos = testPoint;
      return true;
    }
    // 继续下一次 March
    lastPoint = testPoint;
  }
  return false;
}

Depth Mipmap 加速 Ray Marching

在 SSR 的 ray marching 中,步长短了会导致要走很多步,消耗很多性能;而步长长了则可能会导致越过原本应该相交的地方后面,导致错误的相交。

为了优化这一过程,我们可以对 depth buffer 做成特殊的 mipmap,低层级的将取高层级若干个 texel 的最大值,而不是传统 mimap 所取的平均值。这样我们可以先在底层级的 mipmap 进行大步的 march:若没碰到,则说明不在当前这块 texel 的任何子像素,可以继续下一大步;若碰到了,则说明可能与在这块 texel 里的某个子像素相交,因此需要降低层级,进行更小步的 march。

这个 mipmap 加速方法实际上和 BVH 方法是相似的,mipmap 每个 texel 相当于每个AABB包围盒,层级越低则包围盒越大

mip = 0;
while(level>-1)
    step through current cell;
	if(above Z plane) ++level;
	if(below Z plane) --level;

Edge Fading

由于 screen space 的方法天生丢失了屏幕以外的信息,在某些时候的渲染可能会看到反射物比较突兀的断掉了屏幕外的信息:

为了掩盖这一突兀的artifact,可以使用基于像素uv坐标的间接光照权重贡献,即uv坐标越接近边界(例如接近u=0、u=1、v=0、v=1),则权重贡献应当越小:

BRDF 重要性采样

为了让 SSR 的采样更容易收敛,我们可以根据不同的 BRDF lobe 在进行 importance sampling:

射线结果重用

当 pixel 的 ray marching 得出一个相交点时,不仅计算出对该 pixel 的间接光照贡献,还可以将计算该点与原 pixel 附近的 pixel 的间接光照贡献并赋给相应的 pixel :

预过滤采样结果

每个方向采样得到的结果将根据不同的 BRDF lobe 来决定这个结果的权重,从而最终综合得到一个过滤后的间接光照结果,减少了采样的 noise 问题:

SSR/SSRT 的应用与缺陷

缺陷:

  • screen space 方法仍然缺失了屏幕所看不到的几何信息
  • diffuse 情况下,由于要往半球范围均匀采样(不能像specular/glossy那样用importance sampling极大优化采样),容易造成nosiy结果,这时候可能需要牺牲更多的性能来采样更多

应用:

  • SSR 的渲染效果非常好(前面的方案看起来总像是增强部分的图像效果)
  • 通过不同的 brdf 函数,可以自由调成各种反射效果(specular/glossy/diffuse)

参考

posted @ 2021-10-31 21:12  KillerAery  阅读(1532)  评论(0编辑  收藏  举报