Cocos Creator Shader 入门 (21) —— 高斯模糊的高性能实现

💡 本系列文章收录于个人专栏 ShaderMyHead

💡 本文案例可以在 Github 上进行演示

一、仅通过着色器实现的高斯模糊

在《高斯模糊和径向模糊的实现》一文中我们已经介绍了高斯模糊的原理和着色器实现,其原理为通过正态分布规律,分配权重来混合每个像素各方向附近的像素色值。

下图为常规在水平和垂直方位做混合处理的高斯模糊实现流程:

如果希望实现一个高质量的实时模糊效果,一般是需要对被模糊物体进行多次采样的 ------ 采样次数越高,模糊的效果越细腻,但性能也越差。

下方为一台红米低端机的表现情况:

读者可以在线上演示页进行体验。

其表现为:

  • 在不应用模糊特效时,可以稳定在 60 FPS 的帧率;
  • 在「单次采样 10 次(双方位共采样 20 次)」的情况下,帧率降到 50 FPS 左右,且画面存在可察觉的晶格方块;
  • 在「单次采样 30 次(双方位共采样 60 次)」的情况下,画面模糊效果非常细腻,但游戏帧率只能维持在 30 FPS 左右。

GPU 负载可以被简单地量化为公式:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 负载 = 像素数量 × 采样次数 × 刷新频率 负载 = 像素数量 \times 采样次数 \times 刷新频率 </math>负载=像素数量×采样次数×刷新频率

就能看出着色器在单次 DrawCall 里采样的次数所造成的 GPU 负载,会是线性提升的关系。 这兴许也是 Cocos Creator 没有内置高斯模糊组件的原因 ------ 怕被滥用导致游戏性能崩溃。

在移动端图形开发中,纹理采样通常是仅次于 Overdraw(过度绘制)的性能杀手。对于中低端手机而言,在全屏分辨率下进行采样,单次 DrawCall 内采样次数与性能的影响大致如下(仅供参考):

  • 安全区10 次以内

    • 这是标准 Shader 的开销,几乎所有手机都能跑满 60 帧;
  • 警戒区20~30 次

    • 中低端机型开始出现发热,或者在画面复杂时掉帧到 45-50 左右;
  • 卡顿区40~60 次

    • 可能会导致千元机掉帧到 30 帧以下,甚至严重发热;
  • 幻灯片区70次以上

    • 如果不降分辨率,在低端机上会表现得异常卡顿,甚至高端机也会因为带宽过热而降频。

💡 对于游戏画面是静态、可预知的场景,可以不走着色器方案,改而让美术同学提供一张模糊后的图片叠在顶层即可。例如下方是一个仅使用图片实现的高斯模糊、遮罩方案:

然而在大部分游戏场景中,需要被高斯模糊的画面内容都是不可预期的,那么这种纯图片的高性能模糊方案便难以应用。

二、用小尺寸 Render Texture 放大画面

在往期的着色器案例中我们经常使用 Render Texture 来存储摄像机捕获的离屏画面,常规而言会给 Render Texture 配置一个和拍摄画面一致的尺寸。

那么如果一开始就给 Render Texture 设置一个很小的尺寸(例如拍摄画面的 1/10),再以拍摄画面的完整尺寸进行渲染,是否就能达成一个放大、高斯模糊的效果?

下方图片是对这个有趣的想法的实践:

可以看到画面随着 Render Texture 被放大变得更加模糊,但是质量也变得更差、出现越来越明显的方块。

这个问题其实也很容易理解 ------ 原本画面里的细节、边缘、光影变化,全部在缩小时被抹除掉了,相当于我们拿一张很小的图片强行把它进行放大:

然而该方案有个巨大的优势 ------ 可以大幅缩减 GPU 需要采样的像素点,从而进一步减轻 GPU 负载,因此我们可以将其利用起来,只要避免放大太多倍即可。

三、Render Texture 放大 + 着色器低采样的高性能方案

当 Render Texture 只放大五倍时,会产生模糊程度较明显、失真程度也尚可接受的效果:

如果此时再搭配着色器低采样(单方向仅采样 10 次,双方位共 20 次),便能达到一个质量良好的高性能模糊方案:

我们可以使用前文提及的 GPU 负载公式来量化此方案的性能优化成果:

  • 仅使用着色器采样实现同效果的高斯模糊时,采样次数大概需要共 40 次,则 <math xmlns="http://www.w3.org/1998/Math/MathML"> G P U 负载 = 像素数量 × 40 × 刷新频率 GPU 负载 = 像素数量 \times 40 \times 刷新频率 </math>GPU负载=像素数量×40×刷新频率;
  • 使用此方案时,采样次数大概需要共 20 次,像素数量仅为 1/5,则 <math xmlns="http://www.w3.org/1998/Math/MathML"> G P U 负载 = 像素数量 / 5 × 20 × 刷新频率 GPU 负载 = 像素数量 / 5 \times 20 \times 刷新频率 </math>GPU负载=像素数量/5×20×刷新频率;

假设刷新频率一致,那后者的 GPU 负载仅为前者的 1/10,这是个极大的性能优化了。

在安卓低端机中,该方案依旧可以保持在 60 FPS 左右的帧率:

四、烘焙并复用模糊效果,让高性能和高质量并驾齐驱

对于要求比较苛刻的美术同学而言,上一节的高性能方案可能无法达成其满意的视觉效果(如果仔细看确实还能看出一些方块)。

对此我们可以在前文方案的原理上做进一步优化处理 ------ 对高负载的处理进行「烘焙」和「时间切片」处理。

4.1 烘焙和复用单帧模糊效果

我们先继续维持上一节的「Render Texture 放大 + 着色器采样」的方案,不过我们把 RT 仅放大 2 倍(减少失真),并将着色器单次采样的数量从 10 次增加到 20 次(让模糊效果更细腻),效果如下:

此举虽然进一步提升了高斯模糊的质量(模糊的效果已经非常细腻了),但定然也会大幅增加 GPU 的负载,然而重点来了 ------ 我们只需对画面进行单次模糊处理(处理后不再进行着色器采样),在后续一直复用这一帧的画面即可。

其实现方案并不玄乎 ------ 新增一个摄像头用于捕获模糊后的离屏画面到 Render Texture 并在最顶层新增一个 Sprite 节点渲染出来,然后使用 scheduleOnce 在下一帧禁用新增的摄像头、之前进行着色器采样的节点即可:

js 复制代码
    // 启用单帧模糊烘焙
    startBaking() {
        // 略...
    
        // 在下一帧关掉摄像头,并隐藏所有使用着色器的节点
        this.scheduleOnce(() => {
            this.singleFrameCamera.enabled = false;   // 关闭新增的摄像头
            this.BGHoriNode.active = false;           // 禁用水平方位采样节点
            this.BGVertNode.active = false;           // 禁用垂直方位采样节点
        });
    }

此时虽然新增的摄像头失效了,但其捕获的高斯模糊画面依旧留存在对应的 Render Texture 中(并渲染在顶层 Sprite 节点),因此能在后续一直看到这一高质量的高斯模糊画面,且不再进行任何的着色器采样(性能极高):

可以看到在安卓低端机中能实现高质量的、实时的单帧模糊(留意会维持烘焙的静态画面),同时保持在 60 FPS 的游戏帧率。

如果你需要被模糊的画面是不可预知的,但又是静态的,那么该方案会特别适合你。

4.2 通过时间切片实现动态模糊

上一小节的方案有个明显的问题 ------ 原本的动画被截停了,如果需要实现一个实时的、动态的、高质量且高性能的高斯模糊效果,该方案就无法使用。

我们先了解一个有趣的事实 ------ 人眼对高频细节(清晰的物体)的运动极其敏感,但对低频细节(模糊的物体)的运动感知非常迟钝,即使在 3A 主机游戏中(如《赛博朋克2077》、《最终幻想》等),也经常使用「前景 60 帧 + 背景 30 帧」的混合渲染技术。

这意味着被模糊的画面(通常都是背景)其实无需高帧率更新

基于这一点,我们可以利用时间切片的方法,在 update 生命周期里限定单帧烘焙方法以 30 帧的频率来执行:

js 复制代码
    private _timer: number = 0;
    private _bakeInterval: number = 1 / 30; // 锁定 30 FPS 的模糊效果刷新频率
    
    update(deltaTime: number) {
        this._timer += deltaTime;
        if (this._timer >= this._bakeInterval) {
            this._timer = 0;
            this.startBaking();  // 定时烘焙
        }
    }

理论上对于高斯模糊的背景,将更新频率降低到 15 FPS 用户也未必看得出来。

这意味着 GPU 仅会在每 33ms 左右才进行一次高斯模糊采样处理,对于绝大部分手机而言,33ms 是一个足够大的窗口期了。

最终,通过多管齐下的方案搭配,我们得以在安卓低端机上也实现 60 FPS 左右的高质量模糊动画:

💡 本文的图片会被平台压缩丢失质量,建议读者在线上演示页进行体验。

相关推荐
维维酱1 小时前
Vite 构建中的两个典型问题:代码分割命名与循环依赖
前端
前端加油站1 小时前
使劲折腾Element Plus的Table组件
前端·javascript·vue.js
ze_juejin1 小时前
Angular的Service创建多个实例的总结
前端
十五喵1 小时前
智慧物业|物业管理|基于SprinBoot+vue的智慧物业管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·毕设·智慧物业管理系统
特级业务专家1 小时前
React vs Vue 调度机制深度剖析:从源码到事件循环的完整解读
前端
ze_juejin1 小时前
Angular中懒加载模块的加载顺序总结
前端
天蓝色的鱼鱼1 小时前
写Tailwind CSS像在写屎山?这锅该不该它背
前端·css
#做一个清醒的人1 小时前
【Electron】IpcMainEvent 参数使用总结
前端·electron
月弦笙音2 小时前
【包管理器】pnpm、npm、cnpm、yarn 深度对比
前端