Screen Space Reflection Cover

Screen Space Reflection

Screen Space Reflection While screen space reflection (SSR) is a well-known effect, this article aims to introduce a unique method for calculating screen space reflections – one that I haven’t encountered online before. Many online tutorials cover screen space reflection already, and most of them follow a similar process: calculate the reflection direction in world space, use a mostly uniform step size to traverse the world space, and for each step, compute the normalized device coordinates (NDC)....

March 9, 2024 · zznewclear13
Screen Space Reflection Cover

屏幕空间反射

屏幕空间反射 屏幕空间反射也是一个老生常谈的效果了,但正如本博客的宗旨,要从千篇一律中脱颖而出,这篇文章也将介绍与众不同的,至少我在网上没有见到过的计算屏幕空间反射的方法。 网上有很多很多的屏幕空间反射的教程,绝大部分的流程是这样的:计算世界空间的反射方向,使用一个大部分情况下是统一的步长在世界空间中步进,对于每一次步进,计算标准化设备空间的坐标,将当前的深度和深度图进行比较,如果在深度图之后,认为发生了交叉,采样当前点的颜色值并返回。这种方法能看到很多很多看上去非常完美的反射效果,但几乎没有人会提及所需要的步进次数,因为它往往高得惊人,关于这点我们后续还会谈到。而且对于不同远近的物体,想要达到比较好反射效果,其需要的步长往往是不同的,也很少有人去做这方面的思考。稍好一些的会考虑在交叉之后做几次二分法查找,这样能够让一段一段的反射后的颜色带上下颠倒,使画面看上去更加连贯,后面也能看到对比。还有一些会考虑在计算标准化设备空间的坐标后,根据坐标和[-1, 1]之间的大小关系,提前结束步进或是对反射的颜色和环境反射进行插值。目前看来最好的步进方法,是预先计算Hierarchical ZBuffer,通过对更高LOD步进的方法,使用更少的步进次数达到同样的步进效果,但是Hierarchical ZBuffer并不是一个所有项目都能有的特性。 网上能找到的最有用的教程,是Morgan McGuire写的Screen Space Ray Tracing。在他的这篇文章中也提到了为什么在世界空间中步进是不好的,因为世界空间步进的位置在经过透视变换后,很有可能在屏幕空间中没多大变化,也就导致了世界空间步进需要更多的次数来达到较好的反射效果。在这篇文章中展示了一个非常好的方法,计算裁剪空间和屏幕空间的起点和终点的坐标,通过对裁剪空间的z、裁剪空间的1/w、屏幕空间的xy进行线性插值,省去了每一次步进所需要的矩阵运算,十分值得使用。 本文的目标是,在一个Shader中使用尽量少的步进次数得到正确的反射颜色。随机采样、模糊、菲涅尔效应之类的不在本文的考虑范围之内。本文仅考虑Windows平台下DX11的Shader,这样能省去很多的平台适配的代码,使用的Unity版本是Unity 2022.3.21f1,在文章的最后会附上最终的Shader代码。 反射的计算 参数的选择 计算反射基本上只需要三个参数,一个是Max Distance,只考虑距离反射点一定范围内的物体带来的反射,一个是Step Count,更多的步进次数带来更精确的反射,同时也增加性能消耗,最后一个是Thickness Params,对于一个物体,默认其厚度为depth * _Thickness.y + _Thickness.x,这样当射线经过物体背面时不会被认为发生了交叉。 深度比较 步进的时候比较什么深度也是一个值得思考的问题。将步进的深度记为rayDepth,将采样获得的深度记为sampleDepth,一个很简单的想法在标准化设备空间进行比较,因为直接采样深度图就能获取到标准化设备空间的深度值,当rayDepth < sampleDepth的时候,射线和场景发生了交叉。又或是对实际的深度进行比较,这样能够指定一个厚度,当深度的差大于厚度时,认为射线从场景物体的后面穿了过去并没有发生交叉,当rayDepth > sampleDepth && rayDepth < sampleDepth + thickness的时候,射线和场景发生了交叉。此外裁剪空间的Z分量也能用来判断是否发生了交叉,这里不再赘述。深度图的采样方式则应该使用PointClamp的方式,使用线性插值的话在一前一后的两个面的边缘很可能会被认为发生了交叉,导致画面上有不少的小点,除非另外有一张标记物体边缘的贴图可以用来排除掉这部分的交叉点。 光线步进 伪代码很简单: 记k0、k1分别是步进起点和终点的裁剪空间坐标的w分量的倒数。 记q0、q1分别是步进起点和终点的裁剪空间坐标的xyz分量。 记p0、p1分别是步进起点和终点的标准化设备空间坐标的xy分量。 记w是一个在(0, 1)之间按照1.0f/_StepCount递增的变量。 对每一次步进,更新w的值,并对上面的三组分量线性插值得到k、q、p。 使用q.z * k获得rayDepth,使用p采样深度图获得sampleDepth。 如果rayDepth < sampleDepth,射线和场景发生了交叉,跳出循环,返回p。 使用p采样颜色图,获得反射的颜色。 效果是这样的(步进次数为32次): 看上去非常糟糕,最明显的是拉扯的效果。它主要有两个产生的原因:一是我们并没有使用厚度来判断射线是否从物体的背面穿过,这导致了悬空的物体下方会有很长的拉扯;二是我们并没有对超出屏幕范围的位置进行限制,这导致了我们使用屏幕外的坐标采样深度图但返回了Clamp之后的深度值。 厚度检测 为了解决上面的厚度问题,我们新增了一个方法由于判断步进的位置是否在物体后面。我们需要使用的是距离相机的线性深度linearRayDepth和linearSampleDepth。上文说到我们使用linearSampleDepth * _Thickness.y + _Thickness.x来作为一个场景中一个物体的厚度,我们只需要判断(linearRayDepth-linearSampleDepth-_Thickness.x) / linearSampleDepth和_Thickness.y的大小即可,如果前式大于后式,则表明射线从物体后面穿过。 float getThicknessDiff(float diff, float linearSampleDepth, float2 thicknessParams) { return (diff - thicknessParams....

March 9, 2024 · zznewclear13
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics