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
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
Gaussian Blur Cover

使用Group Shared Memory加速高斯模糊

2023年4月5日更新 再议高斯模糊,更实用的高斯模糊。 为什么要用Compute Shader来做高斯模糊 在之前的博客中我说Compute Shader的优势就是快,因为GPU的并行运算比CPU强大很多。但是相比于同样运行在GPU上的Fragment Shader之类,Compute Shader是不是就毫无优势了呢?答案是否定的,在DX11的文档中我们可以看到Thread Group Shared Memory这么一个概念,Compute Shader的Thread Group中的每一个Thread,都可以极快速的访问到对应的Group Shared Memory中的数据,这个效率比采样一张贴图来的高。因此在进行高斯模糊这样的需要大量贴图采样的计算时,先将贴图数据缓存到Group Shared Memory中,再多次访问Group Shared Memory,这样运行的效率会高得多。 相关的一些参考可以在英伟达的PPT里找到。 Compute Shader进行高斯模糊的具体操作 这里暂时不使用半分辨率的优化方法,目的是把SrcIden对应的RenderTexture经过高斯模糊储存到DestIden中,这时我们需要一张临时的相同大小的RenderTextureBlurIden。 确定需要的最大的高斯模糊的像素宽度MAX_RADIUS,越大的高斯模糊宽度,需要越大的Group Shared Memory,而Group Shared Memory的大小是有上限的(cs_5.0是32768 bytes)。当然也没有必要在全分辨率的情况下做特别大的高斯模糊就是了。这里我设置最大的高斯模糊像素数是32,即高斯模糊当前像素点和最远采样点之间的距离不能超过32(双线性采样的话还要缩小一个像素)。 高斯模糊往往使用水平和竖直两个高斯核心进行模糊,对应的需要两个Compute Shader Kernel(也可以写成一个,不过思考起来有点绕),我们这里设置两个Kernel,对应水平和竖直两个pass。水平Kernel使用[numthreads(64, 1, 1)],最高可以是1024,竖直Kernel则使用[numthreads(1, 64, 1)]。 由于高斯模糊中会采样像素点的左右(上下)两侧的像素,Group Shared Memory需要在GroupThreads的基础上向两侧扩大最大高斯模糊像素数MAX_RADIUS,用于保存额外的像素数据。这时我们需要的Group Shared Memory的大小是numthreads + 2 * MAX_RADIUS个,在本文章中是64 + 2 * 32个float3的数据。 GaussianBlur.cs 这里略去Unity SRP的设置pass的操作,仅展示高斯模糊相关的操作。当blurRadius过大时,就能看到明显的多重采样的痕迹了。 private void DoGaussianBlurHorizontal(CommandBuffer cmd, RenderTargetIdentifier srcid, RenderTargetIdentifier dstid, ComputeShader computeShader, float blurRadius) { int gaussianBlurKernel = computeShader.FindKernel("GaussianBlurHorizontalMain"); computeShader.GetKernelThreadGroupSizes(gaussianBlurKernel, out uint x, out uint y, out uint z); cmd....

August 19, 2021 · zznewclear13
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics