CocosCreator高斯模糊深度优化版|征稿活动V5

一、前言

本文着重围绕如下几点讲解:

  • 高斯模糊的基础实现
  • 高斯模糊的线性分解
  • 高斯模糊的双线性采样
  • cocos后处理应用

为了喜迎cocoscreator3.6.2,本示例工程已升级到3.6.2

二、效果预览

↑未模糊画面

↑9x9高斯核4次迭代效果

三、前期准备

首先参考了大佬陈皮皮的帖子,

↑模糊前

↑模糊后

效果确实可以,也能做到深度模糊的效果,但是性能比较堪忧,我界面的分辨率是960x640,设置的模糊半径是20的话,每一帧的计算量是960x640x41x41= 10.3亿 次。(ps:为啥是41? 半径是20,原点是1,就是20+1+20,半径为R,就是(2R+1)x(2R+1)的高斯核),反正是把我的电脑干趴了,将半径调成4,半径4的情况下,计算量是 4976万 次,效果就是上面截图的效果。那有没有优化空间呢?(由于每次乘960和640难看出数据变化,后续的计算量都除掉这两个值)

四、核心思路

高斯模糊在图像处理领域,通常用于减少图像噪声以及降低细节层次,以及对图像进行模糊,其视觉效果就像是经过一个半透明屏幕在观察图像。

从数字信号处理的角度看,图像模糊的本质一个过滤高频信号,保留低频信号的过程。过滤高频的信号的一个常见可选方法是卷积滤波。从这个角度来说,图像的高斯模糊过程即图像与正态分布做卷积。由于正态分布又叫作“高斯分布”,所以这项技术就叫作高斯模糊。而由于高斯函数的傅立叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波器。

说到高斯模糊,就得说到高斯核,高斯核的一个基础模型如下

↑高斯函数的三维示意图

输入的每个像素点计算时都会将该像素周围一圈的像素点(模糊半径)通过基于高斯核的权重计算一遍然后加起来当做输出值。

高斯模糊也可以在二维图像上对两个独立的一维空间分别进行计算,即满足线性可分(Linearly separable)。这也就是说,使用二维矩阵变换得到的效果也可以通过在水平方向进行一维高斯矩阵变换加上竖直方向的一维高斯矩阵变换得到。从计算的角度来看,这是一项有用的特性,因为这样只需要 M * N * m + M * N * n 的计算复杂度,而原先的计算复杂度为M * N * n * m ,其中M, N是需要进行滤波的图像的维数(像素),m、n是滤波器的维数(模糊半径)。

以下为一个Gaussian Kernel的线性分解过程:

上图是滤波5次,杨辉三角展示了二项式系数,它可以用来计算卷积核权重(每个元素是上一排的两个相邻元素的和)。

↑杨辉三角

我们以最下面一行当做数据样本,最下面一行的数字和是4096,因为1/4096和12/4096的值比较小,我们为了保持更加nice的效果,可将1和12的参数去掉,那数字和就成了4070,每个的权重就是[66,220,495,792,924]/4070。

五、实现过程

因为目前cocos 还不支持后处理,所以我们弄个相机将场景渲染成renderTexture,再用Sprite装载,最后用Canvas的相机渲染到屏幕,在测试时发现了一些小问题,流程可以参考我之前发的帖子

我们以总权重为4070的9 X 9高斯核开始学习。

5.1、N X M -> N + M

        _BlurOffsetX: {value: 0,editor: { slide: true, range: [0, 1.0], step: 0.0001 }}
        _BlurOffsetY: {value: 0,editor: { slide: true, range: [0, 1.0], step: 0.0001 }}

[image]

首先定义2个uniform去记录单个像素的uv偏移,做了2个进度条可以动态调整水平和垂直方向的uv偏移,其中size是屏幕的尺寸,因为进度条是从0~1的,所以除以size后就变成了单个像素的uv偏移(ps:我这里用2做了缩放,调到最大相当于每次的偏移量是2个像素,只是为了测试效果)。

顶点着色器没做任何修改,片段着色器代码如下,因为是9X9 所以

  vec4 GaussianBlur() {
     // 原点
      vec4 color = 0.2270270270 * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0);
      // 右边/上方的采样点
      color += 0.1945945946 * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0 + vec2(1.0 * _BlurOffsetX  , 1.0 * _BlurOffsetY ));
      color += 0.1216216216 * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0 + vec2(2.0 * _BlurOffsetX  , 2.0 * _BlurOffsetY ));
      color += 0.0540540541 * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0 + vec2(3.0 * _BlurOffsetX  , 3.0 * _BlurOffsetY ));
      color += 0.0162162162 * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0 + vec2(4.0 * _BlurOffsetX  , 4.0 * _BlurOffsetY ));
      // 左边/下方的采样点
      color += 0.1945945946 * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0 + vec2(-1.0 * _BlurOffsetX  , -1.0 * _BlurOffsetY ));
      color += 0.1216216216 * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0 + vec2(-2.0 * _BlurOffsetX  , -2.0 * _BlurOffsetY ));
      color += 0.0540540541 * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0 + vec2(-3.0 * _BlurOffsetX  , -3.0 * _BlurOffsetY ));
      color += 0.0162162162 * CCSampleWithAlphaSeparated(cc_spriteTexture, uv0 + vec2(-4.0 * _BlurOffsetX  , -4.0 * _BlurOffsetY ));
      
      return color;
  }

[image]

将进度条拉到最大,我们得到了一次模糊的效果,效果如下(这个效果其实是有问题的,后面会讲)

↑一次模糊

这个效果跟9x9的模糊效果基本一致,但是计算量从9x9=81缩减到了9+9=18

5.2、多次滤波

想要实现多次滤波,必须是经过一次滤波之后,将本次输出图片的结果当做参数传到第二次滤波,在unity里用后处理可以轻松的操作,但是cocos creator后处理的功能还在重构中,只能使用另外一种方案:分层+多相机。

在项目设置中,我加了8个Step,并在Canvas地方摆放了8个精灵,每个精灵对应一个Step,并创建了8个摄像机分别去拍这8个精灵,并且摄像机的渲染优先级从低到高,这样8个摄像机总共可以滤波8次。这里我参考了社区的佛光普照教程,这个demo真是强大,当我对cocos的后处理处于迷茫状态时,这个demo对我来说所得即所需。这里封装好的代码我就不细说了,可以直接下载demo了解。

于是我按上面的代码,对模糊结果处理了4次,得到如下的效果:

↑四次模糊

我发现有模糊后的细节发生了扭曲,这肯定是哪里出了点问题,我们回到之前看到的公式 NxN 转化成 Nx1 与1xN,是需要Nx1计算完了再去计算1xN的,而我目前的算法是2者一起计算,用大白话说,原本只计算了水平模糊,再计算垂直模糊,现在我水平和垂直交替处理,那在第二次迭代时,本应该只计算垂直方向的像素值已经被第一次迭代给污染了,导致最终结果有点不一致,所以模糊结果发生了扭曲。

5.1、修正后的多次滤波

所以我们的代码要进行修正,下面是修正后的代码:

我们先用一个pass只处理水平方向的,再用一个pass处理垂直方向的,水平、垂直交叉出现,我将这个理解成乒乓交叉。虽然经过4个pass处理,但是它其实还只是算2次迭代。效果如下:

↑采用乒乓交叉的两次迭代

这张图对比上一张图,我们可以看出前所未有的丝滑,而且模糊效果也比最初的强一些,所以随着迭代次数的增加,模糊效果也会增加,那我就试试4次迭代。得到的效果如下:

↑采用乒乓交叉的四次迭代

此时的模糊程度比最初的效果强得多,此时的计算量是9x4x2 = 72 次,比原本的81次还要少,更少的计算量却能得到更好的模糊效果,但是你以为这就完了嘛?

5.4、线性采样

到此为止,我们假设了必须要做一次贴图读取来获得一个像素的信息,意味着9个像素需要9次贴图读取。尽管这对于在CPU上的实现来说是成立的,但在GPU上却不总是这样。这是因为在GPU上可以随意地使用双线性插值(bilinear sampling)而没有什么额外的负担。这意味着如果不在纹素中心读取贴图,就可以得到多个像素的信息。既然已经利用了高斯函数的可分离性,实际上是在1D下工作,双线性插值会提供2个像素的信息。每个纹素贡献对颜色的贡献量则由使用的坐标决定。

通过正确地调整贴图读取的坐标偏移,可以仅通过一次贴图读取得到两个像素或纹素的准确信息。这意味着为了实现一个9x1或1x9的高斯滤波器只需要5次贴图读取。总的来说,实现Nx1或1xN的滤波器需要[N/2]次贴图读取。

怎么理解这一句话呢?

于是根据上面的公式,算出A点的坐标是1.3846153846 B点的坐标是3.2307692308

将其带入到之前的片段着色器中,代码如下:

↑双线性采样

这时候你会发现跟之前的结果几乎一样,但此时的计算量是5x4x2 = 20

如果去掉4次迭代的效果,只需要原本的9x9的效果,计算量只需要5x2 = 10 次 是不是极大提升了性能呢?

六、最终代码

完整工程 git@gitee.com:onion92/cocos3d-shader-effect.git

七、小结

如果你需要深度模糊的效果,不妨增加迭代次数,或者加大采样半径,采用双线性采样,即使是13x13的高斯核,一次迭代也只需要7x2=14次计算。

八、后话

停了几个月的笔了,前段时间项目比较赶,在项目中做了一个高斯模糊和径向模糊,刚好这次cocos有个征文大赛以及项目出现真空期,于是就有了这篇帖子。(ps:上班的时间用来写文章什么的,最喜欢了)

本来想写个高斯模糊、径向模糊后,再搞个远景模糊的,结果一个高斯模糊下去竟然意想不到的发现了双线性采样带来的优化以及后处理的首次真正使用(以前搞的renderTexture只能处理一次,算啥后处理嘛),嗯,看来回顾旧的知识还是会有提升的嘛,温故而知新,古人诚不欺我!

有兴趣的同学可以关注一下我的公众号。你们的支持是我创作的动力!

泉小墨

感谢各位的观看,希望在渲染的道路上与君共勉,相互成长!

16赞

不明觉厉!ctrl c+v就对了

1赞

大佬 666

有没有2.x版本的

mark
备用.
谢谢.

2.x的算法一样
3.x也是将摄像机的内容渲染到图片上对图片进行的操作

可恶,图没有了

明明是用显卡的香气来将内容渲染的 怎么就成了摄像机了 :rofl: :rofl: :rofl: :rofl:(手动狗头

讲解的非常详细 :+1:t3:

大哥 图全部没有了

[Violation] ‘setTimeout’ handler took 53ms
pacer-web.ts:60 [Violation] ‘requestAnimationFrame’ handler took 266ms
256[.WebGL-00006C9C02F29500] GL_INVALID_OPERATION: Feedback loop formed between Framebuffer and active Texture.
localhost/:1 WebGL: too many errors, no more errors will be reported to the console for this context.

警告比较多

图没有了,学不到了,demo在哪下载呢