Radial Blur Cover

使用Group Shared Memory加速径向模糊

径向模糊 在写了上一篇文章径向分派Compute Shader之后,很自然的就需要用径向模糊来检验我们的算法。 径向模糊的效果在网上可以说是一找一大堆了,百分之九十五的教程,会使用一个很大的循环重复采样纹理,百分之四点九的教程,会使用多pass的方法减少采样的次数(当然这些数字是我瞎编的,我还留了百分之零点一,以防真的有人也用Compute Shader做了径向模糊)。这篇文章就来探讨如何使用Compute Shader中的Group Shared Memory来加速径向模糊的计算。我们的目标是在一个Compute Shader中实现超大模糊距离(甚至能超过128像素)的径向模糊。 缓存颜色信息 照例我们要再看一遍索尼的Bend Studio的Graham Aldridge在Sigraph 2023的索尼创作者大会上,介绍的往日不再(Days Gone)中计算屏幕空间接触阴影的方式,这里可以找到演示文稿和参考代码。演示文稿的第24-25页介绍了如何在Group Shared Memory中缓存深度信息,当然在径向模糊中我们缓存的是颜色信息了。 下图是缓存颜色信息的示意图,绿色方块对应的像素是我们整个Thread Group的起点,红色方块是我们当前计算的像素,黑色和灰色的线代表了该像素到径向模糊中心的方向。我们的Thread Group Size是12,深蓝色(包括绿色和红色)的像素同属于一个Thread Group,我们需要计算这些像素径向模糊后的颜色。深蓝色、浅蓝色、灰色和浅灰色、镂空的灰色和浅灰色,代表了我们需要缓存的颜色,因为我们需要对缓存的颜色进行双线性插值,我们除了需要缓存射线经过的最近的像素(即蓝色区域)外,还需要缓存射线经过的第二近的像素(即灰色区域)和射线经过的第三近的像素(即镂空灰色区域)。也就是说当Thread Group Size为12时,我们需要缓存6 * 12个像素,亦即每个像素六次采样。 仅缓存第二近的像素在大部分情况下能够得到正确的双线性插值,但是注意看红色方块向右的第二次采样,仅对蓝色和实心灰色区域的颜色进行插值是不能得到正确的颜色的,因此我们需要额外缓存镂空灰色区域的颜色,亦即第三近的像素。 Group Shared Memory分配 如上所述,我们的Group Shared Memory的大小是THREAD_COUNT * 6(懒得把THREAD_COUNT统一改成THREAD_GROUP_SIZE了),其中包含了上述的深蓝色、浅蓝色、灰色、浅灰色、镂空灰色、镂空浅灰色区域对应像素的颜色信息。数组的长度是THREAD_COUNT * 6,在我们缓存时,我们会将其视为6行THREAD_COUNT列的表来储存颜色信息,而在我们读取时,我们会将其视为3行THREAD_COUNT*2列的表来读取数据。 groupshared float3 cachedColor[THREAD_COUNT * 6]; void SetCachedColor(float3 color, int2 threadPos) {cachedColor[threadPos.x+threadPos.y*THREAD_COUNT]=color;} float3 GetCachedColor(int2 threadPos) {return cachedColor[threadPos.x+threadPos.y*THREAD_COUNT*2];} 具体的代码 RadialDispatchComputeShader.compute 核心的代码来自于上一篇文章径向分派Compute Shader。循环读取颜色值的方式有很多,可以严格按照格点来读取,这样只需要对y方向做线性插值,也可以按照统一的步长来读取,这样需要对xy方向都做线性插值,我这里使用的是统一的步长。每一组缓存的起始像素是最接近于射线的像素的下面一个像素,读取缓存使用的偏移是用的当前采样点下第一个像素的坐标和当前缓存列最下面像素的坐标相减,这里面比较绕不太好描述。。。 #pragma kernel RadialBlurMain // #pragma warning(disable: 3556) #define THREAD_COUNT 128 Texture2D<float4> _ColorTex; RWTexture2D<float4> _RW_TargetTex; float2 _CenterPosSS; float4 _TextureSize; float _Intensity; struct DispatchParams { int2 offset; int count; int stride; int xMajor; }; StructuredBuffer<DispatchParams> _DispatchData; int GetDispatchType(int index, out int dispatchIndex, out DispatchParams dispatchParams) { for (int i=0; i<8; ++i) { dispatchParams = _DispatchData[i]; dispatchIndex = dispatchParams....

March 31, 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
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
Circular Blur Cover

Unity两个Pass的圆形模糊

圆形模糊 圆形模糊,在Photoshop里又称镜头模糊(Lens Blur),和景深结合在一起的时候被称作散景(Bokeh),是指在摄影时失焦的区域产生的和光圈的形状一致的模糊效果,五边形八边形或是圆形都有可能。 在计算机图形学中实现景深效果基本上有两种方法:第一种也是最常用的,通过黄金率生成一系列的采样点,使得其形状接近想要模糊的形状,这种方法需要很多很多的采样点,基本上找到的都是60次以上的采样次数,由于采样点的分布不一定正好在像素点中心,也不能轻易地使用Group Shared Memory进行优化,事实上大的模糊半径很可能导致Group Shared Memory的大小不够;另一种是针对于特殊的模糊形状,比如正六边形,可以使用三次(MRT的话可以认为是两次)1D的模糊来组合而成,可以在Colin Barré-Brisebois的博客Hexagonal Bokeh Blur Revisited中看到详细的说明,值得一提的是他此前也在EA工作过(看来EA是真的很喜欢散景啊)。 EA的渲染工程师Kleber Garcia在2018年的GDC演讲Circular Separable Convolution Depth of Field中提到了通过复数的运算来实现圆形模糊的算法,其背后的数学我这里就不再赘述了,感兴趣的话可以参考Circularly symmetric convolution and lens blu这篇文章。圆形模糊的参数的生成的代码可以在Kleber Garcia的公开仓库里找到。Kleber Garcia本人也在Shadertoy上写了具体的圆形模糊的代码。 Circular Dof 由于景深效果相对来说比较复杂,这里就只考虑对整个屏幕施加相同程度的圆形模糊效果。 具体的实现方法 其实大部分和之前的高斯模糊没有什么差别。在分离卷积圆形模糊的算法中,圆形的效果是通过多个Filter叠加而成的,每个Filter对应实部和虚部两个参数。以本文为例,本文使用了两个Filter,对于一个Filter的一个颜色分量,需要储存实部虚部两个数据,总体就需要2x3x2=12个通道,使用三张R16G16B16A16_SFloat就能储存所有的数据。在Kleber Garcia的演讲中他还提到了,可以使用bracket的方法,将颜色的卷积数据储存到另一张图中,这样中间的Filter的结果就会落在[0, 1]的范围内,就能使用R8G8A8B8来储存了,可以节省一半的带宽(但是颜色的卷积数据不也要一张32位的图吗,这里我没太懂,感觉优化了但又没那么优化,索性就没那么做)。 整体的操作是:1. 采样源图片,对每个Filter和每个颜色分量计算实部和虚部的值,水平累加后储存到中间贴图中;2. 采样中间贴图,对每个Filter和每个颜色分量计算实部和虚部的值,竖直累加后乘上各自的权重就得到最终的颜色了。由于我没有使用bracket的方法,Filter中会有负值存在,在仅使用两个Filter且半径较大且像素颜色过亮的时候,由于banding的存在会使最终的颜色出现负值,解决方法是在读取颜色的时候做一次Clamp或者是ToneMapping到合理范围。 CircularBlurFilterGenerator.cs 改写自Kleber Garcia的公开仓库。 /* Copyright 2023 zznewclear13 (zznewclear@gmail.com) Copyright 2016 Kleber A Garcia (kecho_garcia@hotmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software....

April 9, 2023 · zznewclear13
Gaussian Blur Cover

再议高斯模糊

将近两年之后再回过头来制作高斯模糊 虽然两年前已经写过了使用Group Shared Memory加速高斯模糊这篇文章了。但当时写的时候仍有一些遗憾的地方,由于使用的是长度为17的静态的高斯模糊的数组(实际上只有9个权重),虽然在一定程度上能够达到任意调节高斯模糊的程度的效果,但在较低程度的高斯模糊时,是通过手动线性插值找到合适的采样颜色,且一定会有17次的颜色和权重的运算;而在较高程度的高斯模糊时,由于仅有十七个有效的颜色点,会有明显的采样次数不足的瑕疵。 而这两年之间我也曾考虑使用不同的方法来制作一个既能满足很高程度的高斯模糊,又能兼顾很小程度的高斯模糊,性能上也相对高效,且使用同一套通用的代码,的高斯模糊效果。下面便是我之前在Shadertoy上写的通过随机采样和历史混合的高斯模糊效果。 Stochastic Gaussian Blur 但随机带来的噪声和历史混合带来的限制,决定了这种方法终究不能真正地使用在项目中,于是我又开始回到了使用Compute Shader和Group Shared Memory来计算高斯模糊效果的老路子上。不同的是,这次我使用了Compute Buffer把高斯模糊的参数传给Shader,这样就能确保范围内的每一个采样点都能够对最后的颜色产生应有的贡献。 正态分布(Normal Distribution) 和之前不同的是,这次我们要先从正态分布入手,从正态分布的特性来考虑我们的计算方式。正态分布的概率密度函数(probability density function)如下所示: $$ f(x) = \frac 1 {\sigma \sqrt{2 \pi}} e^{- \frac 1 2 (\frac {x-\mu} \sigma)^2} $$ 使用正态分布对信号进行过滤,被称作高斯滤波器(Gaussian Filter)。我们在使用的时候会把\(\mu\)设成0,这样永远是最中心的信号带来最大的贡献。但是这个概率密度函数的\(x\)的范围是\((-\infin, \infin)\),我们不可能对所有的信号都进行采样,于是我们一般对\(3\sigma\)范围内的信号进行采样,对1D的正态分布来说,\((-3\sigma, 3\sigma)\)占据了约99.7%的面积。因此我们往往使用三倍的\(sigma\)作为采样的半径,事实上在2D的时候,可能需要更大的采样半径才能消除明显的采样半径过小的瑕疵。 有一点值得一提的是,虽然我并不会具体的微积分的计算,但据我所知先后执行两个\(\sigma\)值分别为\(x\)和\(y\)高斯模糊,等价于执行一次\(\sigma\)值为\(\sqrt {x^2+y^2}\)的高斯模糊。 另一个有趣的点是,在普通的模糊操作是我们往往会用降采样再升采样的方式来减少采样的次数。对于半分辨率的线性1D降采样和升采样,中心像素保留了\(\frac 3 8\)的之前像素的信息,我们可以找到那么一个\(\sigma\)的值使得其在\((-0.5, 0.5)\)之间的面积约等于\(\frac 3 8\),这样我们就能说我们通过线性降采样和升采样做到了近似对应\(\sigma\)的高斯模糊的效果。可惜这个\(\sigma\)不太好算,有Group Shared Memory也没有必要去做额外的降采样和升采样了。 在本文中,会通过横竖两个1D高斯滤波器来等效一个2D的高斯滤波器,使用Group Shared Memory的话,倒是一个2D的高斯滤波器效率更高一些,不过为了后续的扩展性,本文拆成了两个滤波器。 具体的实现方法 剩下的就和之前大同小异了,为了确保每个像素只会进行至多两次采样,需要限制高斯模糊的最大半径GAUSSIAN_BLUR_MAX_RADIUS为THREAD_GROUP_SIZE的一半。而为了2D的高斯模糊在比较极端的情况下也能有比较好的效果,我的高斯模糊的半径会是\(\sigma\)的3.8倍向上取整。 GaussianBlurComputeShader.compute 这是一个横竖两次高斯模糊的Compute Shader,通过Group Shared Memory优化了原本高斯模糊的每个像素的采样操作(至多两次)。最大模糊半径为128个像素。 #pragma kernel GaussianH #pragma kernel GaussianV Texture2D<float4> _SourceTex; RWTexture2D<float4> _RW_TargetTex; StructuredBuffer<float> _GaussianWeights; float4 _TextureSize; #define GAUSSIAN_BLUR_MAX_RADIUS 128 #define THREAD_GROUP_SIZE 256 const static int CACHED_COLOR_SIZE = THREAD_GROUP_SIZE +GAUSSIAN_BLUR_MAX_RADIUS*2; groupshared half3 cachedColor[CACHED_COLOR_SIZE]; void SetCachedColor(half3 color, int index) { cachedColor[index] = color; } half3 GetCachedColor(int threadPos) { return cachedColor[threadPos + GAUSSIAN_BLUR_MAX_RADIUS]; } void CacheColor(int2 groupCacheStartPos, int cacheIndex, int isHorizontal) { int2 texturePos = groupCacheStartPos + cacheIndex * int2(isHorizontal, 1 - isHorizontal); texturePos = clamp(texturePos, 0, _TextureSize....

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