所谓基于屏幕,就是指利用的信息来源于“屏幕”,例如: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} ight|^{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 的思想:
- 阴影生成 pass:在光源摄像机渲染 shadow map (往往只记录了深度)的时候,顺便额外记录 世界坐标 (x_p) 、法线 (n_p)、 接受的直接光源 radiant flux (Phi_p)。那么就可以认为 shadow map 的一个 texel 对应一块patch ,从而这张 shadow map 就包含了所有次级光照 patch 的信息了 。
实际上,世界坐标也可以通过uv坐标、遮挡深度来推算得到,好处是可以节省空间,坏处是在后面的pass需要渲染 pixel 时,大量对纹理的采样会导致大量的坐标变换计算(而且很多计算都是重复的),因此在RSM算法中,不推荐这种压缩做法。
此外,计算一个 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 上的纹理坐标。
- 主渲染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_{ ext {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} ight), t+r_{max } xi_{1} cos left(2 pi xi_{2} ight) ight))
(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:
- RSM 假设次级光源面均是 diffuse 的,这会影响图像 visibility 的正确性(当然大部分情况下,)
应用:
- 作为廉价的GI方法,常被用于做单个重要光源的GI效果(例如手电筒)
Screen Space Ambient Occulsion(SSAO)
屏幕空间环境光遮蔽(Screen Space Ambient Occulusion,SSAO):是一类游戏工业界很常用且廉价的屏幕空间GI方法。
所谓环境光遮蔽(AO),就是某个 shading point 因为被其它几何表面所遮挡,从而降低了接受外界环境光的比例(这种遮蔽常常发生在凹处表面):
一种计算AO的经典方法就是通过蒙特卡洛+ray casting 预计算模型上各点的AO,然后做成 AO 纹理可以运行时像普通纹理一样采样并与颜色相乘(AO map 存的是 visibility 值)。
SSAO 将要用到的屏幕信息是:color、depth
SSAO 不需要预计算过程,只需要通过屏幕空间信息就能做到还算不错的AO效果:
-
在第一个 pass 只渲染整个场景的直接光照,得到包含直接光照结果的 color buffer 和 depth buffer。
-
在第二个 pass 对整个屏幕渲染,对于某个 shading point ,在该点周围随机采样一些点,然后这些点与 depth buffer 对应的深度作比较:若采样点的深度小于 depth buffer 对应位置的深度,则说明该采样点被遮蔽了。而这些采样点的遮蔽率便是该 shading point 的遮蔽率。遮蔽率将乘于 color 得到该 shading point 最终的渲染结果。
当然也有不正确的遮蔽现象,例如下图中间点的采样,有个红色采样点实际上没有被遮蔽。但是该采样点的深度小于depth buffer的对应深度,因此被 SSAO 判定为遮蔽了。
SSAO 效果图(左为关闭SSAO效果,右为开启SSAO效果,可以看到物体交界处等地方多了更多的暗部细节):
SSAO Blur
实践中由于性能限制,SSAO 一般仅使用16个采样点,那么 AO 的结果将会是 noisy 的:
这时候就稍微修改下 SSAO 的算法流程,在计算 shading point 的 AO 时,不再直接乘于 color。而是先写入到一个 AO buffer 上,之后用一个屏幕后处理 pass 对 AO buffer 信息进行边缘保留滤波算法(其实就是保持边缘感的模糊操作,例如双边滤波算法),那么得到将是不那么 noisy 的 AO 结果:
Horizon Based Ambient Occlusion(HBAO)
实际上,shading point 的 SSAO 采样范围不应该是一个球型,而应当是基于该点的法线为中心的半球形采样范围(因为渲染方程本就是上半球的积分,下半球的光线不会照到 shading point )。
HBAO 就是采样上半球采样范围的 SSAO 改进方法,得到该范围的采样点算法也很简单:
vec3 rand; // 在球形上的随机坐标
vec3 n; // shading point法线
rand = sign(dot(n,rand))*rand; // 在半球上的随机坐标
SSAO 的应用与缺陷
缺陷:
- 仅包含屏幕表面的几何信息不能表示完全正确的 visibility,因此 AO 效果不那么准确(相对于预计算AO贴图)
应用:
- 廉价的GI效果,提升画面的暗部细节,大部分游戏都会将其纳入一种画面增强选项。
Screen Space Directional Occlusion(SSDO)
Screen Space Directional Occlusion(SSDO) 也是一类与 SSAO 极其相似的屏幕空间GI方法,区别在于它们看待光线遮蔽的角度是相反的:
-
AO 认为 shading point 朝外的光线打到物体几何表面时,相当于外部的直接环境光被这个表面遮挡了,因此(对于下面这幅图) AO 将红色部分视为间接光照来源,黄色部分视为损失的间接光照
-
而 DO 认为 shading point 朝外的光线打到物体几何表面时,相当于受到了间接光照(光照来源于打到的表面),因此(对于下面这幅图) DO 会将黄色部分视为间接光照来源,红色部分视为损失的间接光照
用渲染方程去表示两种GI就是:
(L_{mathrm{SSAO}}left(mathrm{p}, omega_{o} ight)=int_{Omega^{+}} L_{mathrm{environment}}left(mathrm{p}, omega_{i} ight) * f_{r}left(mathrm{p}, omega_{i}, omega_{o} ight) cdot V(p) cdot cos heta_{i} mathrm{~d} omega_{i})
(L_{mathrm{SSDO}}left(mathrm{p}, omega_{o} ight)=int_{Omega^{+}} L_{mathrm{indirect}}left(mathrm{p}, omega_{i} ight)* f_{r}left(mathrm{p}, omega_{i}, omega_{o} ight) cdot (1-V(p)) cdot cos heta_{i} mathrm{~d} omega_{i})
其中,(V(p)) 代表 (p) 周围采样点被 depth buffer 深度遮挡的概率。
因此 SSAO 往往增加的是明暗细节,而 SSDO 往往增加的是周围物体表面的颜色影响(或者说增加color bleeding效果)
SSAO 将要用到的屏幕信息是:color、depth
SSDO 算法流程:
-
在第一个 pass 只渲染整个场景的直接光照,得到包含直接光照结果的 color buffer 和 depth buffer。
-
在第二个 pass 对整个屏幕渲染,对于某个 shading point ,在该点周围随机采样一些点,然后这些点与 depth buffer 对应的深度作比较:若采样点的深度大于 depth buffer 对应位置的深度,则说明该采样点将提供间接光照。而这些采样点的间接光照按权重加起来便是该 shading point 的间接光照结果。间接光照结果将直接叠加 color 得到该 shading point 最终的渲染结果。
SSDO 效果图:
SSDO 的应用与缺陷
缺陷:
- 仅包含屏幕表面的几何信息仍然不能表示完全正确的 visibility
- 仅支持短距离GI效果,而无法展示长距离的GI
- 会缺失屏幕看不到的平面信息(对于有颜色的GI效果很容易看出artifact)
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 的算法流程:
-
在第一个 pass 只渲染整个场景的直接光照,得到包含直接光照结果的 color buffer 、normal buffer、 depth buffer。
-
在第二个 pass 对整个屏幕渲染,对于某个 shading point ,在该点往半球随机方向投射若干条射线(使用 ray marching算法),然后将与射线相交的点 (mathrm{p'}) 将对 shading point 的间接光照做出贡献(这与渲染方程是一致的):
$ L_{mathrm{indirect}}left(mathrm{p}, omega_{o} ight) = int_{Omega^{+},V=1} L_{}left(mathrm{p'}, omega_{i} ight) * f_{r}left(mathrm{p}, omega_{i}, omega_{o} ight) cdot cos heta_{i} mathrm{~d} omega_{i}$
其中当射线命中时, (V = 1) ;否则,(V = 0)
为了减少计算,这里仍然假设次级光源点是 diffuse 的,这样式子实际可以写成:
(L_{mathrm{indirect}}left(mathrm{p}, omega_{o} ight) = int_{Omega^{+},V=1} frac{E(mathrm{p'})}{pi} * f_{r}left(mathrm{p}, omega_{i}, omega_{o} ight) cdot cos heta_{i} mathrm{~d} omega_{i})
此外,SSR 还可以通过使用不同的 brdf 来实现不同的反射效果:
SSR 效果图:
SSR的 Ray Marching
得益于带 depth buffer,SSR 可以实现比较廉价的 Ray Marching 效果。Ray Marching 的精度和性能之间的平衡将取决于 march 的步长。
算法先从 start point 开始,
- 每次往射线方向走一个步长得到一个测试点,将该测试点变换成屏幕坐标 ((u,v,z))
- 根据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)