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
High Quality Bloom Cover

Unity的高质量的Bloom效果

Bloom辉光效果 一直都想做一个Bloom效果,Bloom是一个很简单的效果,几乎所有介绍后处理的教程里都会提到Bloom效果的制作,但是Bloom又是一个不那么简单的效果,大部分教程制作出来的Bloom看上去都不太好看。 想要做好Bloom,首先得认识到什么是Bloom效果。Bloom是由于透镜不能完美地让光线聚焦与同一点而导致图像上的高亮区域的颜色向周围溢出的效果,和体积雾这样的由于多次散射和折射形成的溢出效果在原理上就不相同。在计算机图形学里往往使用多次模糊的方式来表现这种效果。 而在讨论什么是好的Bloom之前,我们先来看看差的Bloom的效果。Matthew Gallant在他的文章Bloom Disasters中就给出了很多当时的极糟糕的Bloom效果的例子。可以看到Bloom很重要的一点是,Bloom之前的画面必须要是HDR的画面,如果整个画面被限制在01之间,那么白色的T恤和特别亮以至于看上去是白色的太阳带来的Bloom效果就会相同。在LearnOpenGL上有那么一篇文章,其中说到,为了模拟我们眼睛的工作原理,我们不对颜色进行阈值限制,而是直接对HDR画面进行模糊再和原HDR画面进行插值。我认为这是一种十分错误的方式。最合理的方式应当是,画面上的每个颜色确实会向周围溢出自己的颜色,但是更亮的颜色的溢出半径会更大,对于较暗的颜色,由于溢出半径小于半个像素宽,在最后的画面中就看不到颜色的溢出了。但是根据明度来控制溢出的半径是一件很复杂的事情(这和景深的原理是一样的,所以我到现在都没有掌握一个很好的景深的算法),因此我们在计算的时候通过仅模糊超出阈值的颜色来模拟这种效果。模糊半径也是一个决定Bloom质量的关键要素,如果模糊的半径比较小,看上去就像高光套了一个稍弱的圈一样,不够美观。 Jorge Jimenez在2014年Siggraph多的Advances in Real-Time Rendering课程上介绍了他为使命召唤现代战争所做的次世代后处理效果。他的PPT里介绍了使命召唤现代战争中运动模糊、散景、次表面散射、Bloom和阴影采样的做法,十分值得一看。本文在整体的算法上就使用了他介绍的方法,而采样则使用了Dual Kawase Blur的算法,可以看我之前的文章。 Bloom的算法 主流的Bloom算法都会使用一个阈值,第一个Pass提取出大于这个阈值的颜色(使用减法,这样能够和小于阈值的颜色形成自然的过渡),然后进行一系列的降采样升采样以减少采样的次数,最后将之前所有的升采样的模糊结果(就相当于是Mip的每一级)叠加到一开始的颜色上。为了减少最后一步采样所有的Mip等级带来的消耗,根据Jorge Jimenez的做法,我们会在每一步升采样时叠加当前Mip的颜色。但是最后叠加不是一个很好的处理方法,由于叠加了各个Mip的颜色,会导致原来高光的区域的亮度会被提高到原来的两倍甚至更多,不过我们之后的Tone Mapping能够一定程度上缓解这个问题。然后是对微小的高亮物体的处理,Jorge Jimenez使用了1/(1 + Luma)的方式进行加权处理,不过如果我们将Bloom移动到TAA之后,这个问题能够很好的解决掉。至于模糊,在我之前那么多文章的铺垫下,也就不是什么难点了。 我自己在实现的时候,会在最后一步叠加Mip到最一开始的图像时,将模糊后的颜色除以所有的降采样次数,这样能够稍微弥补一下多个Mip带来的亮度剧烈增加的问题。事实上我也想过将因为阈值而丢失的亮度储存在透明通道里,和颜色一起参与模糊,在最后的时候加回之前丢失的亮度,最后和原始颜色线性插值,不过似乎不那么好做。 这里可以对比一下Unity自带的Bloom和我的Bloom之间的效果差异。Unity第一个Pass预过滤会进行13次采样,之后每一次降采样分成横竖两个方向,横向9次采样,竖向5次采样,升采样则是在2次采样中线性插值。我的则是每次降采样进行5次采样,每次升采样进行8+1次采样,不需要分横竖采样。下图上边是Unity自带的Bloom,下边是我的Bloom,最后均使用Aces Tonemapping,场景里的大立方体的大小是相邻小立方体的1.5倍,而小立方体的亮度是相邻大立方体的1.5倍。我尽量地将参数调的差不多,Unity一共22个Draw最高Mip为7,我的一共17个Draw最高Mip为8,可以观察到Unity的会有稍微明显一点的Banding,中间亮度的中间大小的物体带来的Bloom比我的稍微大一些。 Unity Default Bloom My High Quality Bloom Bloom的具体实现 HQBloomComputeShader.compute 这里有四个Kernel,HQBloomWeightedDownsample用于第一次降采样时减去阈值并加权进行模糊,HQBloomDownsample是和Dual Kawase Blur一样的降采样的模糊,HQBloomAdditiveUpsample是在Dual Kawase Blur的升采样的基础上和低一级的Mip叠加,HQBloomComposite则是将最低一级Mip和原始颜色进行混合。 #pragma kernel HQBloomDownsample #pragma kernel HQBloomWeightedDownsample #pragma kernel HQBloomAdditiveUpsample #pragma kernel HQBloomComposite #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" Texture2D<float4> _SourceTexture; Texture2D<float4> _ColorTexture; RWTexture2D<float4> _RW_TargetTexture; SamplerState sampler_LinearClamp; float4 _SourceSize; float4 _TargetSize; float _Threshold; float _InvDownsampleCount; float _BloomIntensity; float3 applyThreshold(float3 color, out float luma) { luma = Luminance(color); return color * max(0....

July 22, 2023 · zznewclear13
Relaxed Cone Step Mapping Cover

在Unity里实现松散圆锥步进Relaxed Cone Step Mapping

阅读前提示 由于本文使用的贴图均为LearnOpenGL网站上的贴图,其法线贴图和一般Unity或Unreal引擎中的法线贴图的Y分量是相反的,因此在计算世界坐标的bitangent的时候会额外再乘上一个sign,在正常情况下是不需要的。 视差效果 在三角形面数比较受限的情况下,往往会考虑使用一张高度图,通过视差的计算去渲染出一种3D的效果(虽然现在直接用曲面细分Tessellation似乎是一种更普遍的且更有效的方法)。有两种计算视差的方法,一种叫做Parallax Occlusion Mapping,先假定高度的层数,然后对每一层计算出合适的位置和颜色,从而达到3D效果;另一种叫做Cone Step Mapping,是根据高度图预先计算出每个点对于其他所有像素的最大的圆锥张角(有点像AO),根据圆锥张角快速步进,最后使用二分法计算出最终的交点的颜色。第一种方法有一个比较大的缺点,就是在视角比较接近平面的时候,如果采样次数不是很高,就会看到一层一层的效果,可以通过对最后一次计算深度进行线性插值在一定程度上减轻一层一层的问题;第二种方法的缺点是,当采样次数较小时,产生的图像会有一定程度的扭曲,但不会有一层一层的感觉,此外相较于第一种会有一个优点,较细物体不会被跳过。在GPU Gems 3中提到了一种Cone Step Mapping的优化,叫做Relaxed Cone Step Mapping,相较于之前计算最大张角的方式,这种优化通过确保通过圆锥的射线与圆锥内部的高度图至多只有一个交点,减少了一开始圆锥步进的次数。本文就主要使用这种方法进行计算,也许将圆锥的顶部放在比当前高度图更深的位置能够更加减少步进的次数,不过我稍微尝试了一下好像效果并不是特别理想。 Parallax Occlusion Mapping可以在Learn OpenGL里找到介绍和优化方案,Shadertoy上也有开源的代码可以参考。UE5中有一个叫Get Relief!的插件,可以用来快速生成Relaxed Cone Step Mapping的预计算的贴图,也提供了渲染的Shader。这个插件的作者Daniel Elliott也在GDC2023上分享了制作的思路,如果链接打不开的话这里还有一个GDC Vault的链接。 本文使用的贴图可以在Learn OpenGL中给出的下载链接中找到。为了看上去舒服一些,这里对displacement贴图的颜色进行了反向。 下图是两种视差做法的比较,左边是Parallax Occlusion Mapping,右边是Relaxed Cone Step Mapping,两者的采样次数是相同的,可以看到POM在较极限的情况下会有分层感而RCSM会有扭曲。RCSM使用的贴图也放在下面了,R通道是高度图,G通道是圆锥的张角。本文使用的是Unity 2021.3.19f1c1。 生成预计算的贴图 和Parallax Occlusion Mapping直接使用深度图不同的是,Cone Step Mapping需要预先计算出一张圆锥张角的图,圆锥的张角可以使用圆锥底的半径除以圆锥的高来表示,记为coneRatio。本文中使用的是高度图,但实际计算中会使用1减去高度值,对应的是从模型表面到实际高度的深度值。由于深度值只会在01之间,uv也只会在01之间,因此对于最深的点,其最大的圆锥张角不会大于1。 “确保通过圆锥的射线与圆锥内部的高度图至多只有一个交点”,对于圆锥顶部的currentPos和圆锥底部的rayStartPos(这个圆锥是一个倒立的圆锥,其底部和模型表面相平),可以采样一个目标点cachedPos,当cachedPos的深度小于currentPos的深度时,沿着cachedPos - rayStartPos的方向移动cachedPos的位置并一直采样所有像素samplePos,直到samplePos的深度值小于cachedPos(即射线穿过高度图并穿出),根据samplePos和currentPos就能计算出一个圆锥的张角coneRatio。循环所有的像素就能得到最小的圆锥张角了。 为了减少单次计算的消耗,本文会先将整张图片分成NxN大小的区域,在一次循环中会计算所有像素对于这NxN大小的区域的圆锥张角,循环所有的区域就能得到最后的圆锥张角了。同时只需要让N等于THREAD_GROUP_SIZE,就能使用group shared memory仅通过一次采样缓存这些区域的深度值。再有就是Early Exit的优化,当cachedPos在贴图外部,当cachedPos的深度大于currentPos的深度,当cachedPos的圆锥张角大于当前最小的圆锥张角,在这些情况下可以直接结束向外步进的循环。更多的优化方法也都能在Get Relief!的分享中找到。 具体的代码 RCSMComputeShader.compute 用于生成Relaxed Cone Step Mapping的贴图。PreProcessMain用于处理最一开始的深度图,预先设置最大的coneRatio为1。Early Exit是减少运算时间的关键。 #pragma kernel PreProcessMain #pragma kernel RCSMMain #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" Texture2D<float4> _SourceTex; RWTexture2D<float4> _RW_TargetTex; SamplerState sampler_LinearClamp; float4 _TextureSize; float2 _CacheOffset; #define THREAD_GROUP_SIZE 16u [numthreads(8, 8, 1)] void PreProcessMain(uint3 id : SV_DispatchThreadID) { uint2 tempID = uint2(id....

July 1, 2023 · zznewclear13
Dual Kawase Blur Cover

几乎连续的双Kawase模糊

2023年6月3日修订 发现还是在降采样升采样后进行线性插值来获取中间程度的模糊效果比较好,所以后面的代码也都改过来了,不过理论上的部分倒是没必要改。顺便也花了点时间写了一个Shadertoy作为演示: Dual Kawase Blur 写这篇文章的原因 网上已经有了很多很多的双Kawase模糊的现成的案例,但是存在以下几个问题:1. 绝大部分的文章都只给了代码,没有相应的解释,至多会给一张直接从Arm的pdf截取的图示,而这张图示画了一堆方框和符号,却没有说明这些图案代表的含义。2. 绝大部分的文章通过修改采样的距离来控制模糊的程度,这个的缺点我们后续再谈。3. 绝大部分的文章并没有考虑模糊程度从0开始逐渐增大的动态过程,使用降采样和升采样往往会破坏整个画面的连续性。 如果只是想要获得一个模糊的画面,只需要做几次降采样和升采样就能完成了,但我希望能有一个连续地逐渐地变模糊的过程,因此我开始了量化双Kawase模糊的想法。 双Kawase模糊(Dual Kawase Blur) 双Kawase模糊是2015年Arm在Kawase模糊的基础上提出的一种通过降采样和升采样来快速且高效地进行高质量大半径模糊的一种方法,具体的pdf可以从这里找到。 这里是一张双Kawase模糊的图示,表示了双Kawase模糊在降采样和升采样时的操作。细的黑线对应的格子是原始的像素(或是升采样后的像素),粗的黑线对应的格子是降采样后的像素。叉对应的是当前模糊的像素,圆对应的是当前模糊的像素所需要采样点。粉色对应的是降采样时的模糊的像素和采样点,绿色对应的是升采样时的模糊的像素和采样点。 从这张图中也可以看到双Kawase模糊利用双线性采样来节省采样数的操作。在降采样时实际采样了当前像素周围一共十六个像素的颜色;在升采样时实际采样了当前像素周围一共十三个像素的颜色。而如果在做降采样时,对于奇数个像素除以二向下取整,或者是在降采样时使用了不恰当的偏移(比如1.5倍的偏移),会导致降采样的采样点落在原始像素的中心,这时即使使用了双线性采样,也只等价于采样一个像素。 因此为了让每一个像素都能对模糊做到应用的贡献,为了达到比较好的模糊效果,我们这里限制双Kawase模糊的采样偏移为一倍(也就是严格按照采样点进行最优的双线性采样)。而通过多次降采样和升采样达到合适的模糊半径。 量化双Kawase模糊 降采样和升采样有一个缺点,就是只要发生了降采样和升采样,就必然会带来模糊。这时有两种方法,一种是在原始分辨率下通过消耗更大的方式进行加权模糊来逼近双Kawase模糊配合降采样带来的模糊;另一种是在零次和一次双Kawase之间线性插值得到一张介于两者之间模糊程度的图像。综合两者来看,线性插值得到的效果更为平滑,效果上稍“错误”一些,但完全在可接受的范围内。 我这边写了一个小小的脚本,去计算原始分辨率下值为1的像素点,在经过一次双Kawase模糊后,其他像素的值。通过多项式拟合这些模糊后的值,就能利用这些值来逼近双Kawase模糊的效果了。我这边对8x8的像素做了计算(实际上模糊的核心应该更大一些,不过我懒得改之前的代码了)。计算出的权重如下: 0.0003255208 0.001464844 0.003092448 0.004231771 0.004231771 0.003092448 0.001464844 0.0003255208 0.001464844 0.004882813 0.009440104 0.01204427 0.01074219 0.007486979 0.004231771 0.001464844 0.004394531 0.01334635 0.02311198 0.02701823 0.0218099 0.01334635 0.007486979 0.003092448 0.01009115 0.02571615 0.03808594 0.04329427 0.03678386 0.0218099 0.01074219 0.004231771 0.01529948 0.03222656 0.04069011 0.04589844 0.04329427 0.02701823 0.01204427 0.004231771 0.01416016 0.0296224 0.03678386 0.04069011 0.03808594 0.02311198 0.009440104 0.003092448 0.007324219 0....

May 29, 2023 · zznewclear13
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics