Screen Space Ray Reflection
1 Summary
本文介绍屏幕空间反射中最重要的一步——基于 hierarchy depth buffer(HZB) 的 ray march算法,主要参考了 [1] 中的实现。
ray march算法的目的是为了根据屏幕空间深度得到光线在屏幕上的交点,相比于世界空间的光追,这是一种开销很低但仅局限于屏幕空间的做法,也就是若光线交于相机看不到的表面,那么屏幕空间ray march则无法得到求交结果。而 hierarchy depth buffer 则提供了四叉树的加速结构,每一级mipmap都取上一级四个深度中的closest depth。假如光线在 closest depth 的表面之上(距相机更近),那么表示光线不会与更精细层级相交,因此可以快速行进离开不会相交的区域。
2 hierarchy depth buffer
hierarchy depth buffer(HZB) 是构建depth buffer的mipmap形式,但与API接口提供的mipmap构建方式(取均值)不同,hierarchy ray march需要每一层级取前一层级四个深度中的closest depth,具体取最小或最大由depth实现决定。本文采用 $[0,1]$ 的depth,越大距相机越远,因此取最小深度。
3 Construct Ray
对于屏幕空间,往往已知着色点像素坐标 fragCoord
,以及基于需要采样的光线方向 rayDir
(位于世界/相机空间,本文采样相机空间)。ray march 过程在屏幕空间进行,同时为了方便在不同mip层级行进,需要得到screen uv space(即 $[0,1]\times [0,1]\times [0,1]$)下的光线起点,以及屏幕空间的光线方向。
构建光线:
- 光线起点
origin
:着色点(fragCoord, depth)
变换到 view space 得到viewRayPos
,变换到 screen uv space 得到光线起点origin
- 光线方向
direction
:view space下沿光线方向行进一个单位得到nextRayPos = viewRayPos + rayDir
,变换到 screen uv space 得到nextRayScreenPos
。光线在屏幕空间的单位方向为direction = nextRayScreenPos - origin
,注意不需要normalize,因为这并不是三维空间的方向。
4 Ray March
给定光线起点 origin
与光线方向 direction
,都位于 screen uv space。在光线行进过程中,会反复执行一下步骤:
- 先行进 linear step,即在当前层级行进一个像素。
- 执行基于深度的求交判断,有:
如果光线从当前像素在表面上(更靠近相机)掠过,表示无交点,那么去更高层级(表示更大步长)。
如果光线与当前像素相交,表示可能相交,那么去更低层级(表示更小步长)
4.1 Linear March
行进一个步长是希望沿着光线方向行进到下一个像素,同时下个像素内执行一个偏移,如图 Fig-1 所示。

行进步长 marchStep
可设置为
1 |
|
像素内部偏移量 uvOffset
以像素大小为单位,可设置为
1 |
|
假设当前光线位置 currentRayCoord
以及当前层级分辨率 currentMipResolution
,linear march的执行如下
1 |
|
在进行求交之前,光线需要先行进一步,避免与自身所在像素自相交,行进到下一个像素的boundary plane处,有
1 |
|
4.2 Intersection
每次迭代都需要进行判断是否与将要行进到的像素有交点,以此来决定下一步应该如何执行。如何判断光线与将要行进像素是否相交?
假设光线当前位置为 positton,将要行进位置为 new position,如图 Fig-1 所示。new position 处的 (x, y, surfaceZ) (surfaceZ为该处像素的深度) 形成了一个bounding box的一半,即相邻的三个面组成的boundary,可以看到 Fig-1 中,俯视近平面所展示的 x/y plane 形成的黄色 boundary plane。从侧面看近平面如图 Fig-2 所示,相机位于上方,两条黄色 boundary line交点处即为 new position。

对于一个像素而言,其内部深度都是相同的,光线是否会与即将行进的像素相交,通过查看两个端点 position与new position即可:
- 光线当前位置 position 与表面的关系有:position.z < surfaceZ,光线在表面之上;否则,光线在表面之下,可能与像素相交
- 求光线与new position形成的boundary plane (x, y, surfaceZ)的最近交点。如果最近交点不在z plane上 (即没有与z plane相交),则表示光线可以从new position上方 (<newPosition.z) 掠过
因此,对于光线当前位置 position 的两种情况而言:
- position.z < surfaceZ:光线在表面之上,不会与position所在像素相交。再来检查与new position的情况
- a. 光线与new position的 z plane 相交,表示光线与下一个像素相交
- b. 否则,即与new position的 x/y plane 相交,表示光线与下一个像素也不相交
- c. 否则,光线在表面之下,与position所在像素相交
为了能够使用 hierarchy depth buffer 加快速度,应该有:
如果本次迭代的行进有可能相交,则去更细粒度层级,即使用更小行进步长。例如:
- 上述情况 a,光线可以行进到new position的 z plane 交点位置;
- 上述情况 c,光线位置不变,等待更细粒度层级测试
如果不会相交,则去更粗粒度层级,即使用更大行进步长。例如:
- 上述情况 b,光线可以行进到new position的 x/y plane 的交点位置。
上述迭代一直进行到没有更细粒度层级可用时,表示已找到可能交点;否则,达到迭代次数上限后,表示无法找到交点。
1 |
|
每次迭代后,调整层级
1 |
|
4.2.1 特殊情况 :confused: :question:
上面描述都是以光线方向背离相机为例,当光线方向朝向相机时,需要特殊注意:
- position.z >= surfaceZ:与前述过程没有不同
- position.z < surfaceZ:此时不能使用 z plane 的交点,因为得到的
4.2.2 线性查找
前面讲到,迭代到没有更细粒度层级时,表示找到交点,退出迭代。但如果是超出最大层级时呢?一种做法是,可以看作无法找到交点,但如果想要查找更充分,此时可以降为在最大层级线性查找过程,迭代后层级调整实现修改如下
1 |
|
4.2.3 交点是否有效
前述讲的求交测试都是基于深度信息进行的,如果光线走到了像素背后,则表示相交。但实际上,像素占据了表面一定面积,而该小部分表面并非与近平面平行或者并非是平坦的,因此所在表面存在一定厚度,有可能光线从表面背后穿过去,只有在像素厚度范围内的光线,才算是相交。因此,在得到交点后,还需要进行验证是否有效。
提取交点 position
处的深度值 surfaceZ
,分别将 position.z
与surfaceZ
变换到线性空间(相机/世界空间),得到 rayDepth
与 hitDepth
,有
1 |
|
4.3 Put All Together
1 |
|
5 Reflection
最后简要介绍下屏幕空间反射的反射部分。屏幕空间反射希望得到金属材质的着色点的反射信息,首先需要在着色点处根据材质信息,重要性采样得到反射光线的方向。以着色点为起点、反射光线的方向组成的光线,在屏幕空间执行ray march,找到的交点即为反射点。接下来,SSR直接将上一帧反射点的颜色(不包含反射)作为反射点向着色点发出的radiance。注意:这是一种简单的近似,并非PBR过程,因为屏幕上的颜色实际上应该是屏幕像素向相机发出的radiance,这里直接近似作为其向着色点的radiance。