HiZ 还在施工中……
0x00 To begin with
这篇文章将会包含以下内容:
- 部分课程内容回顾
- GAMES202 作业 3
For reference👇:
master 分支上是 202 全部作业汇总,working 分支用于提交代码。需要原始作业可下载 master 分支的包
0x01 课程回顾 Recap
SSR(Screen Space Reflection)是一种基于屏幕空间的反射算法,也被应用于模拟全局光照。因为是屏幕空间的算法,不能且无需像传统全局光照算法那样需要获取场景的原始几何信息,而是只在当前相机的视野范围计算反射,所以效率相对较高,但也存在一些屏幕空间的 Artifacts。
SSR 算法有两个假设:
- 屏幕范围内的反射光,其反射源也同样来自屏幕空间的某个像素;
- 所有被直接光照亮的物体(在屏幕空间中则表现为像素),都可以作为次级光源参与对反射的贡献;
SSR 对反射表面没有要求,即不仅能模拟镜面反射,还可以模拟各种粗糙度的反射

SSR 算法的步骤如下:
- 对每个需要计算反射的像素沿反射方向发射一条光线;
- 沿光线方向步进在屏幕空间中查找深度缓冲并与场景求交;
- 若有交点,则将交点颜色作为反射颜色添加到原始像素上;
其中最关键的步骤就是光线求交,最简单的方法就是 Linear RayMarching,光线每次行进一个固定的距离,每次行进后获取该位置的深度值再与场景深度进行比较,如果某次判断光线深度大于场景深度,则认为发生了相交。

当然这种方法的精细度很依赖光线步长,小的步长效果更好但消耗大,反之,更大的步长则可能导致光线“穿过”场景而误判相交,造成错误计算反射的情况。
为了在提高精度的同时节省开销,研究人员又提出了一种动态步长的方法,即 Hierarchical ray trace。允许光线根据场景信息在行进期间选取不同的步长,例图中光线一开始至少可以以 7 倍的 gap 作为步长,之后再缩减到原始的步长。

说到实时渲染中的一些动态操作,几乎就离不开预处理。同样的,这种方法运行的前提是,光线需要知道行进的“最大安全距离”,即可以迈多大的步子而不与场景发生相交。
这里最大安全距离是类比 SDF 的说法,或者应该说当前层级规定的步长
一种实现它的方法是预先生成深度图的 Mip-Map,这里四个像素的最小值作为 Mip 生成的逻辑,具体原因涉及到比较的保号性:如果光线与上一级 Mip 都不相交($D_{ray} 拿到这些 Mip 之后就可以对 RayMarching 算法进行改良:一开始仍然以一个比较小的步长,如果行进后没有交点,则下次步进则在上一个层级进行(增加步长);反之若有交点,则不走这步,回到下一个层级(回退步长)再次尝试步进。 这里层级的上下是基于图像金字塔的位置关系,上面的层级信息更粗糙,下面的层级信息更精细 这种层级方法叫做 HiZ(Hierarchical Z-buffer),能够显著减少 RayMarching 的迭代次数。 直接光照总是比较简单的,而且之前的两个 Pass 已经帮我们把漫反射、世界空间法线和可见性信息(其实就是 ShadowMap)写入了 GBuffer,直接根据屏幕 UV 采样就能获取对应信息。具体而言要完善两个函数: 然后在 具体思路是:对于一条给定的射线,记录它的起点和方向,每次迭代时判断当前射线的起点处的深度与 GBuffer 中记录的深度的大小,如果其相减大于预先设定的阈值,则认为光线在该处相交;反之则继续向光线方向前进一段距离。到达最大迭代次数或找到交点时,算法终止。 这里的参数需要调整至合适值,尤其是步长和阈值 可以写一个函数,假设表面是绝对镜面,检查一下 SSR 的实现,直接返回交点的漫反射颜色作为 SSR 的结果: 这块的逻辑伪代码也给得很清楚了,要注意的有两个点: 对于 1,框架中给出了 一切正常的话可以换个场景看看效果,engine.js 中,修改以下几处地方: 更换光源信息(65-71 行)和加载的模型(90 行): 更换摄像机位置(34-35 行): 去到网页就能看到大概这样的效果,可以看到暗部也有不错的间接光效果,如果觉得噪点太多可以修改采样数或 RayMarching 的参数,但是配置太精细网页会比较卡。

0x02 直接光照 Direct Lighting
EvalDiffuse 和 EvalDirectionalLight。EvalDiffuse 函数返回漫反射 BSDF 的值,注意要把 cos 项算进去:
1vec3 EvalDiffuse(vec3 wi, vec3 wo, vec2 uv) {
2 vec3 L = vec3(0.0);
3
4 vec3 rho = GetGBufferDiffuse(uv);
5 vec3 normalWS = GetGBufferNormalWorld(uv);
6
7 float cosTheta = max(dot(normalWS, wi), 0.0);
8 L = rho * INV_PI * cosTheta;
9
10 return L;
11}
EvalDirectionalLight 返回 Shading Point 获得的 Light 的 Radience,这里考虑阴影,从 ShadowMap 中获取可见性关系,然后直接和 uLightRadience 相乘即可。
1vec3 EvalDirectionalLight(vec2 uv) {
2 vec3 Le = vec3(0.0);
3
4 float visibility = GetGBufferuShadow(uv);
5 Le = uLightRadiance * visibility;
6
7 return Le;
8}
main 函数更新一下 L 的计算逻辑:

0x03 屏幕空间光线追踪 SSR
RayMarch 函数用来判断采样的光线是否与场景相交,相交时将参数 hitPos 设置为交点,并返回 true。参数 ori 和 dir 分别为世界坐标下光线的起点位置和单位方向向量。
1#define MAX_ITERATION 100
2#define INTERSECTION_THRESH 1e-5
3#define RAY_STEP 0.02
4
5bool RayMarch(vec3 ori, vec3 dir, out vec3 hitPos) {
6 vec3 curPos = ori;
7 vec3 rayStride = normalize(dir) * RAY_STEP;
8
9 for(int i = 0; i < MAX_ITERATION; i++){
10 // get GBuffer infos
11 vec2 screenUV = GetScreenCoordinate(curPos);
12 float rayDepth = GetDepth(curPos);
13 float sceneDepth = GetGBufferDepth(screenUV);
14
15 // test intersection
16 if(rayDepth - sceneDepth >= INTERSECTION_THRESH){
17 hitPos = curPos;
18 return true;
19 }
20 curPos += rayStride;
21 }
22 return false;
23}

1vec3 EvalSSR(vec3 wi, vec3 wo, vec2 uv) {
2 vec3 L = vec3(0.0);
3
4 vec3 posWS = vPosWorld.xyz;
5 vec3 normalWS = GetGBufferNormalWorld(uv);
6 vec3 reflectDir = normalize(reflect(-wo, normalWS));
7 vec3 hitPos = vec3(0.0);
8
9 if(RayMarch(posWS, reflectDir, hitPos)){
10 vec2 screenReflectUV = GetScreenCoordinate(hitPos);
11 L = GetGBufferDiffuse(screenReflectUV);
12 }
13
14 return L;
15}
main 函数中再将这个镜反射的贡献加上,最终的效果应该是这样的,可以看到是正确反射了地面的颜色:
0x04 间接光照明 Indirect Lighting
wi 为直接光方向(因为是 Directionnal Light),wo 为投射光线的逆(相当次级光照);LocalBasis 用于计算局部空间中的基向量,给定法线 n,即可构造出局部空间到世界空间的变换矩阵 TBN;至于 2,对次级光源调用 EvalDiffuse 时分别对应设置其 wi 和 wo 即可。
1vec3 EvalIndirectLight(vec3 wi, vec3 wo, vec2 uv, float s) {
2 vec3 Li,L = vec3(0.0);
3
4 // load TBN matrix
5 vec3 n = GetGBufferNormalWorld(uv);
6 vec3 t, b;
7 LocalBasis(n, t, b);
8 mat3 TBN = mat3(t,b,n);
9
10 float pdf = 0.0;
11 vec3 ori = vPosWorld.xyz;
12 vec3 hitPos = vec3(0.0);
13 vec2 screenIndirectUV = vec2(0.0);
14
15 for(int i=0; i<SAMPLE_NUM; i++){
16 // transform from tangent space to world space
17 // this ray cast from pos0 to pos1
18 // pos1 also serve as a light source
19 vec3 rayDir = normalize(TBN * SampleHemisphereCos(s,pdf));
20
21 if(RayMarch(ori,rayDir,hitPos)){
22 screenIndirectUV = GetScreenCoordinate(hitPos);
23 // light is directional hence for each shading point uses the same wi for lightDir
24 L = EvalDiffuse(rayDir,wo,uv)/pdf * EvalDiffuse(wi,-rayDir,screenIndirectUV) * EvalDirectionalLight(screenIndirectUV);
25 Li += L;
26 }
27
28 }
29
30 Li /= float(SAMPLE_NUM);
31 return Li;
32}


