Cocos Shader 基础入门(八):巧用 WebGL 抓帧工具提高渲染效率

上一个章节为止,我们已经将 shader 的基础内容讲得差不多了,最主要的部分就是 WebGL 的应用如何对照到 Cocos Creator。因为所有的游戏开发工具的渲染底层都是图形渲染 API 的封装,所以只要知道了原理,就可以轻松使用不同的游戏开发工具创造出想要的效果。本章将针对之前的内容做一些补充和扩展。

运行时材质修改

在第六章中,我们说过关于材质包含的可配置参数:

  • effectAsseteffectName: effect 资源引用,指定使用哪个 EffectAsset 所描述的流程进行渲染。(必备)
  • technique: 指定使用 EffectAsset 中的第几个 technique,默认为第 0 个。
  • defines: 宏定义列表,指定开启哪些宏定义,默认全部关闭。
  • states: 管线状态重载列表,指定对渲染管线状态(深度模板透明混合等)有哪些重载,默认与 effect 声明一致。

因此,这里尝试着用代码的方式创建之前定义的 shader foo,并将之前我们在编辑器上的操作都用代码的形式设置:

import { _decorator, Component, Node, Sprite, Material, SpriteFrame, gfx, Texture2D, EffectAsset } from 'cc';
const { ccclass, property } = _decorator;
const { BlendFactor, CullMode } = gfx;

@ccclass('Example')
export class Example extends Component {
    @property(EffectAsset)
    foo: EffectAsset = null!;
    @property(Texture2D)
    dissolveMap: Texture2D = null!;

    start () {
        const sprite = this.getComponent(Sprite);
        const mat = new Material();
        // effect name 适用于内置 shader,可以通过 EffectAsset.get('effect name') 方式获取,判断
        mat.initialize({
            effectAsset: this.foo,
            defines: {
                USE_TEXTURE: true,
            },
            states: {
                blendState:{
                    targets: [
                        {
                            blend: true,
                            blendSrc: BlendFactor.SRC_ALPHA,
                            blendDst: BlendFactor.ONE_MINUS_SRC_ALPHA,
                            blendDstAlpha: BlendFactor.ONE_MINUS_SRC_ALPHA
                        }
                    ]
                },
                rasterizerState: {
                    cullMode: CullMode.NONE,
                }
            }
        });

        sprite.customMaterial = mat;
        mat.setProperty('u_dissolveMap', this.dissolveMap!);
        mat.setProperty('dissolveThreshold', 0.3);
    }
}  

如果需要在运行时修改,就可以用下列方式进行动态修改:

// 所有带有材质的组件上都可以获取材质,通过 comp.material 获取出来的材质通常都是材质实例 MaterialInstance
// 材质 Material 和材质实例 MaterialInstance 的区别在于,MaterialInstance 从挂载的那一刻就一直属于唯一的组件,且只对该组件模型生效,而 Material 则无此限制,可以属于多个同类型组件或者不同类型组件
// 只有 MaterialInstance 可以运行时动态修改 defines 和 states

const sprite = this.getComponent(Sprite);
const mat = new Material();
 mat.initialize({
    effectAsset: this.foo,
});
const matInst = sprite.material;

matInst.recompileShaders({ USE_TEXTURE: true });
matInst.overridePipelineStates({
    blendState:{
        targets: [
            {
                blend: true,
                blendSrc: BlendFactor.SRC_ALPHA,
                blendDst: BlendFactor.ONE_MINUS_SRC_ALPHA,
                blendDstAlpha: BlendFactor.ONE_MINUS_SRC_ALPHA
            }
        ]
    },
    rasterizerState: {
        cullMode: CullMode.NONE,
    }
});
matInst.setProperty('u_dissolveMap', this.dissolveMap!);
matInst.setProperty('dissolveThreshold', 0.3);

UBO 内存布局

UBO(Uniform Buffer Object)是 uniform 缓冲区对象。它和普通的 uniform 作用一样,只不过可以一次性管理一个或者多个 uniform 数据。它替代了 gl.uniform 的的方式传递数据,也就不会再占用 shaderProgram 自身的 uniform 存储空间,可以存储更多的 uniform 变量。

提到 UBO 就必须要提到着色语言 GLSL 中的 Uniform Blocks(上个章节提到 uniform 是在 block 内声明 ),它将众多的 Uniform 类型的变量集中在一起进行统一的管理,对于需要大量 Uniform 类型变量的程序可以显著地提高性能。相比传统设置单个uniform类型变量的方式,具有如下几个特点:

  1. 可以存储更多的 uniform 类型变量

  2. 可以简化大量 uniform 变量设置的流程

  3. 可以通过切换不同的 UBO 绑定,在单一着色语言程序中迅速更新程序中的 uniform 类型变量的值

  4. 可以在不同的着色语言程序中通过更新 UBO 中的数据实现所有 uniform 类型变量的更新

// 普通 uniform 声明
uniform float dissolveThreshold;

// UBO 形式声明
uniform Dissolve{
    float dissolveThreshold;
};

在 Cocos Creator 中规定 shader 的所有非 sampler 的 uniform 都应该以 block 形式声明。考量到 UBO 是渲染管线内要做到高效数据复用的唯一基本单位,离散声明已不是一个选项,因此,uniform 声明时对数据分配有更加严苛的要求,比如:

  • 不应该出现 vec3 成员。
  • 对于数组类型的成员,每个元素的 size 不能小于 vec4
  • 不允许任何会引入 padding 的成员声明顺序

其中,padding 的规则是:

  • 所有 vec3 成员都会补齐至 vec4
uniform ControversialType {
  vec3 v3_1; // offset 0, length 16 [IMPLICIT PADDING!]
}; // total of 16 bytes
  • 任意长度小于 vec4 类型的数组和结构体,都会将元素补齐至 vec4
uniform ProblematicArrays {
  float f4_1[4]; // offset 0, stride 16, length 64 [IMPLICIT PADDING!]
}; // total of 64 bytes
  • 所有成员在 UBO 内的实际偏移都会按自身所占字节数对齐
uniform IncorrectUBOOrder {
  float f1_1;                    // offset 0, length 4 (aligned to 4 bytes)
  vec2 v2;                       // offset 8, length 8 (aligned to 8 bytes) [IMPLICIT PADDING!]
  float f1_2;                    // offset 16, length 4 (aligned to 4 bytes)
};                               // total of 32 bytes

// 可以看到,上面这种声明方式, v2 由于对齐的是 vec2,也就是起始计算偏移了两个 float,也就是是 8 字节。所以浪费了 4-7 之间的 4 个字节
// 又由于 f1_1 和 v2 已经占用了一个 vec4 的存储空间,因此,f1_2 需要开启一个新的 vec4 内存空间
//因此,正确的方式如下
uniform CorrectUBOOrder {
  float f1_1; // offset 0, length 4 (aligned to 4 bytes)
  float f1_2; // offset 4, length 4 (aligned to 4 bytes)
  vec2 v2; // offset 8, length 8 (aligned to 8 bytes)
}; // total of 16 bytes

WebGL 抓帧工具

在讲解到 WebGL 抓帧工具之前,我们要先知道什么是 DrawCall。一个 DrawCall 就是 CPU 向 GPU 发送一次绘制指令,如果场景里需要绘制 80 个物体,那就有可能需要提交 80 个左右的 DrawCall,也就是一个渲染帧内 GPU 需要绘制 80 次。DrawCall 过高会直接影响游戏的整体性能,造成卡顿等问题。所以需要知道 DrawCall 信息,尽可能多地将节点数据合并提交。 因此,我们需要一个工具帮助我们分析每个 DrawCall 绘制了什么,方便我们采取适当的方法优化数据,让相同的渲染数据可以合并,这个工具也称之为抓帧工具。

在 Web 上,Spector.js 就是一个很好的 DrawCall 分析工具,可以直接在 google store 上下载。安装这个插件完成后,你可以在 chrome 浏览器右上角看到这个图标。

01

在游戏运行的时候,点击这个按钮,启用抓帧工具。此时的按钮也会处于点亮状态。再次点击该按钮,点击红框处按钮,就会弹出当前帧渲染页。

在这个页面上,你就能看到当前帧渲染到底做了什么。左边可以点击切换不同的绘制指令,同时观察到每一个 gl 指令执行后的界面视图变化;中间执行的 gl 指令;右边则是可以查看一个绘制指令的详细内容,往下滑动可以看到类似 state 的数据,还有提交了 哪些顶点输入属性和 uniform 属性。

如果是绘制指令,还可以在中间的 gl 指令处看到跳转到顶点着色器和片元着色器的按钮。右上角则是一些状态和信息面板切换,大家自行去点击看看就能明白。

从图中可以看出,没有出现 clear 或者 clearColor 之类的指令,这个之前提过,clear 相关的都在相机身上。相机身上有个属性 ClearFlags 就是用来处理 clear 行为的。它们分别的作用是:

  • DONT_CLEAR:不执行任何绘制清除
  • DEPTH_ONLY:只执行深度清除
  • SOLID_COLOR:清空颜色、深度和模版缓冲
  • SKYBOX:启用天空盒,只清空深度

一个场景里必须要有一个相机执行 SOLID_COLOR 操作。由于 Cocos Creator 3.x 作为一个偏重 3D 开发的工具,2D 内容显示在 3D 内容前面,因此,2D 相机不会主动执行 SOLID_COLOR 操作。但是,像我们这次的案例,场景里只有 2D 内容,那么就可以让 2D 相机去执行 SOLID_COLOR,也就是 gl 的 clear 操作。接着,将 2D 相机的 clearFlags 修改为 SOLID_COLOR,此时再观察。

在这里建议大家,使用 Spector.js 时,推荐使用 WebGL1 的渲染后端,可以看到的信息更多一点。WebGL2 会将部分信息省略,无法直观的看出问题。可以通过 项目设置->功能裁剪 选择渲染后端。

「Cocos Shader 基础入门系列」的八个章节已更新完毕, 接下来我会再用两章的篇幅带大家学习一些扩展内容,即渲染的最后一个环节:测试与混合。

要看下一集?戳这里看全系列教程>>>>

6赞

学!废了!

学会了学会了,牛啊

yo yo yo yo yo

学废了学废了

学废了学废了

:innocent:代码块怎么都合并成一行了,有点不方便阅读…

哥,我改了一下,现在很好阅读了,哥快快再看看看~

好的,这样方便多了,辛苦啦

真是难得一见的好文章 支持了 希望多推荐一些工具

vec2 v2;
float f1_1;
float f1_2;
这样是也是16个字节吗