CocosCreator游戏性能优化(2):合批渲染之RenderToTarget

本文从降低drawcall角度来分析如何提升性能。

本文链接 CocosCreator游戏性能优化(2):合批渲染

相关链接 CocosCreator游戏性能优化(1):性能分析工具

               CocosCreator游戏性能优化(3):GPU优化之降低计算分辨率

一、分析Drawcall性能瓶颈

首先,每次CPU向GPU提交渲染指令,都会消耗一系列性能。例如,CPU计算、数据传输IO、GPU申请创建、绑定VBO等对象、GPU程序编译链接损耗。如果多次执行,而GPU每次都处理不饱和,那么会造成性能热点和浪费。

我们看下CPU向GPU提交渲染会有哪些操作:

    1、CPU遍历场景节点树,计算渲染指令。此过程中还会检测合批(如果开启了动态合批)或其他操作。

    2、CPU提交渲染指令到渲染队列。CPU创建或查询需要使用的纹理数据和格式Texture;创建并绑定VertexBufferObject,调用gl接口解析特定格式的顶点数据VertexData;同时创建GPU程序,设定shader uniform值,编译链接shader代码。

/** 解析顶点数据 */

GLuint vbo;  // 声明vbo变量

glGenBuffers(1, &vbo); // 生成1号vbo对象

glBindBuffer(GL_ARRAY_BUFFER, vbo);  // 绑定并使用1号vbo对象

glBufferData(GL_ARRAY_BUFFER, sizeof(float) * M * N, nullptr, GL_STATIC_DRAW); // 向vbo对象中填充顶点数据

glBindBuffer(0, &vbo);  // 解绑 

/** 编译链接GPU程序 */

...

...

二、合批规则和原理

        因此,为了减少上述的计算频率和冗余,我们需要进行渲染合批。单并不是所有纹理都能合批,目前了解至少需要满足以下条件。

        1、使用同一张纹理

        2、纹理颜色、透明度相同。(cocos高版本已经支持,不需要满足此条件)

        3、纹理使用的材质相同

        4、纹理GPU程序Shader相同。包括uniform参数一致

        5、降低打断合批的情况。

        原理是cocos通过上述多种对象和数据来计算最终的哈希值,根据这个哈希值来决定是否能够进行合批操作。

        为了满足以上条件,我们要做的合图或者尽可能多地创造可以进行合图地条件(比如避免合批被打断)。

        打断合批的原因主要有:

        1、渲染顺序。CocosCreator访问节点树是深度优先的,也就是说不同父节点的纹理即使相同,也会被打断。

        2、文字渲染。纹理中间插入了文字,那么也会被打断渲染批次。

        3、材质参数不一致。

/** cc.Material 材质设置参数会重新计算hash值 */

getHash(){

    if(!this._dirty)returnthis._hash;

    this._dirty=false;

    leteffect=this._effect;

    lethashStr='';

    if(effect){

        hashStr+=utils.serializeDefines(effect._defines);

        hashStr+=utils.serializeTechniques(effect._techniques);

        hashStr+=utils.serializeUniforms(effect._properties);

    }

    returnthis._hash=murmurhash2(hashStr,666);

}

二、合批方式

        目前CocosCreator支持多种合批方式。合成大图以后,程序用到的N张小图,如果都在这张大图中,那么提交的纹理地址都指向这张大图,属于同一个,也就利于合批。

        1、静态合批    

            静态合批是指在游戏运行之前,就将图片打包成大图。

            CocosCreator对大图操作比较友好。

            1、提供了是否打大图的可选操作,并且提供预览。缺点是参数支持有限,比如小图间距不可调,不能保留透明边框等。

            2、大图在编辑器中能够直接读取和操作小图,和合图之前操作差别不大,非常方便。

            静态合图方式:

            1、使用cocosCreator编辑器自带AutoAatlas合图。操作如下。

            在需要合图的文件夹下新建一个自动图集配置即可。打包时会将该文件夹下的所有图片合成一张或多张大图。


创建自动图集配置

              2、使用TexturePacker打包大图。优点是功能很丰富,可以设定很多自定义参数。

                    下载地址: TexturePacker Download


        2、动态合批

            动态合批行为发生在程序运行中,程序会根据设定的规则,将小图写入大图目标纹理。在cocosCreator中主要有以下几种动态合批方式。

            1、图片动态合批

                (1)编辑器中的图片的属性检测面板中,勾选Packable选项,表明此图可以应用动态合批规则。

                (2)程序中加入代码cc.dynamicAtlasManager.enabled = true;表明该程序允许进行动态合批操作。

            执行以上操作后,程序就会在运行时将允许合图的小图按优化算法写入大图中。进而达到合批的目的。

            但是要注意,动态合批有一定的规则,比如宽高不能超过一定值等。

           2、自定义合批

            除了cocosCreator自定义的合批方式外,我们还可以自己实现对任意节点树或关心的节点集进行自定义合图。

            优点是可以突破不同层级、不同父节点的限制,缺点是会消耗一部分内存。

            介绍两种比较简单的方式:

            (1)渲染到纹理。利用OpenGL的FBO,存储到RenderTexture,然后使用RenderTexture代理原来节点的显示。在CocosCreator中,FBO的使用可以通过Camera的Render来实现。

            (2)动态修改父节点和节点的顺序。理想的情况主要是将能够合批(见合批规则与原理)的节点放在一起,并将文字节点和图片节点分开放。满足引擎渲染队列广度遍历、以及避免文字会打断合批的情况。

在相对静态或对更改父节点不频繁、不影响游戏流程的情况下,可以采用。此过程可用Spector.js插件来查看实际渲染的内容是否符合预期。

            (3)关于文字,CocosCreator已经提供了三种可选的动态合批模式,分别是None、BitMap、Char模式。其中None标识不合批,BitMap表示将使用到的每个Label转换成texture后,合到一张特定的大图中。Char表示使用到的单个文字转换成texture后,和到一张特定的大图中。由此可见,每个方式各有应用场景。

但是要注意的是,当文本数量或用到的单个字数量超过一定上限(即引擎内部大图不足以填充)的时候,就会出问题。这个可以通过修改该引擎内部的逻辑来优化这个问题。比如新文本texture替换未使用的文本位置的替换算法,复用大图空间。这个以后可以单独开专题讲。

下面重点看下渲染到纹理这种做法。

我们先实现基础截图组件FBOComponent类。

export class FBOComponent extends cc.Component {

    /** 目标源节点 */

    public _inTarget: cc.Node = null; // 摄像机会将该节点渲染到RenderTexture中

    /** 缓存纹理对象 */

    public _renderTexture: cc.RenderTexture = null; // RenderTexture对象

    /** FBO摄像机 */

    public _fboCamera: cc.Camera = null; // cocosFBO的API实现在相机中

    /** 输出sprite */

    public _outSprite: cc.Sprite = null; // 需要使用RenderTexture的精灵组件

    /** 输出是否翻转Y */

    public _isFlipY: boolean = true;

    /** 输出目标的原始scaleY */

    public _scaleY: number = 1;

    /** 是否使用外部renderTexture */

    public _useNewTexture: boolean = false;

    /** 是否每帧绘制 */

    public _frameDraw: boolean = true;

    /** 是否采用late绘制 */

    public _useLate: boolean = true; // 为了数据同步,需要在lateUpdate中进行Camera的Render操作

    /** 仅绘制一帧时,是否可以绘制 */

    public _canOnceDraw: boolean = false; // 某些情形只需要Render一次,提升性能(例如静态或更新不频繁的页面)

默认在lateUpdate中进行RenderTexture绘制。

lateUpdate(): void {

        if (this._useLate) {

            if (this._frameDraw) {

                this.updateFBO();

            }else if (this._canOnceDraw) {

                this._canOnceDraw = false;

                this.updateFBO();

            }

        }

    }


updateFBO() {

        this.onFBOUpdateBegin();

        if (this._fboCamera) {

            if (!this._renderTexture && !this._useNewTexture) {

                this._renderTexture = new cc.RenderTexture();

                this._renderTexture.initWithSize(this._inTarget.width, this._inTarget.height, cc["gfx"].RB_FMT_D24S8);

                this._fboCamera.targetTexture = this._renderTexture;

            }

            if (!this._renderTexture) {

                return;

            }else {

                if (Math.floor(this._renderTexture.width) != Math.floor(this._inTarget.width) || Math.floor(this._renderTexture.height) != Math.floor(this._inTarget.height)) {

                    this._renderTexture.updateSize(this._inTarget.width, this._inTarget.height);

                }

            }

            this._fboCamera.enabled = true;

            this._fboCamera.render(this._inTarget);

            this._fboCamera.enabled = false;

            if (this._outSprite) {

                this._outSprite.spriteFrame = this._outSprite.spriteFrame || new cc.SpriteFrame();

                this._outSprite.spriteFrame.setTexture(this._renderTexture);

                this._outSprite.node.scaleY = this._isFlipY ? -this._scaleY : this._scaleY;

            }

        }

        this.onFBOUpdateEnd();

    }


以上关键代码实现了将任意节点树转换成RenderTexture并更新至目标显示Sprite组件的功能。可以用于需要频繁或每帧进行render的情况。

例如2d游戏中,在存在多障碍物的情况向,移动时,每帧绘制阴影区域的的黑色部分到一张texture中,就可以利用该FBOComponent组件来实现。

需要注意的是,这里没有同步目标节点位置、宽高等,默认是一样的不变的。

那么,我们现在注意到_canOnceDraw这个成员变量,这个成员变量是用来控制单次render功能的。也是用于一些仅需合批一次或按需render的情况。

例如很多行文字的任务列表。只有在收到新任务或完成任务等状态发生变化的时候,才需要重新render一次。

这个时候有可能位置宽高都发生了变化。我们来实现少量绘制和自同步的功能。

以下实现BitmapCacheComponent组件类。它继承于FBOComponent,且实现了drawOnece()方法和自动同步真正用于目标显示的大小、位置等属性。

还可以按自己shader需求可以实现自定义材质属性。

export class BitmapCacheComponent extends FBOComponent {

    /** bitmap父节点 可选参数 */

    public _bitmapParent: cc.Node = null; // 要将真正显示的Sprite放在哪个父节点

    /** 是否启用修复 可选参数 */

    public _enableFix: boolean = false;

    /** 材质数组 可选参数 */

    public _materials: {  // 真正显示的Sprite 需要绑定的自定义材质

        index: number,

        url: string,

        script?: typeof ShaderScript,

        matParams: any,

    }[] = [];

...

}

开始和结束render时的回调函数。在这里进行属性同步。

protected onFBOUpdateBegin() {

        super.onFBOUpdateBegin();

        this._outSprite = this._outSprite || this.initBitMapSprite(this._bitmapParent || this.node.parent);

        this._outSprite.node.width = this.node.width;

        this._outSprite.node.height = this.node.height;

        this._outSprite.node.anchorX = this.node.anchorX;

        this._outSprite.node.anchorY = this.node.anchorY;

        this._outSprite.node.x = this.node.x;

        this._outSprite.node.y = this.node.y;

        this._outSprite.node.opacity = 0;

        this.node.opacity = 255;

    }

    protected onFBOUpdateEnd() {

        if (this._outSprite) {

            let parent = this._outSprite.node.parent;

            let wpos = this.node.convertToWorldSpaceAR(cc.Vec2.ZERO);

            let lpos = parent.convertToNodeSpaceAR(wpos);

            this._outSprite.node.x = lpos.x;

            this._outSprite.node.y = lpos.y;

            this._outSprite.node.width = this.node.width;

            this._outSprite.node.height = this.node.height;

            this._outSprite.node.anchorX = this.node.anchorX;

            this._outSprite.node.anchorY = this.node.anchorY;

            this.node.opacity = 0;

            this._outSprite.node.opacity = 255;

        }

        if (this._enableFix) {

            this.tryFixBitMap();

        }

        if (this._cacheCallback) {

            this._cacheCallback();

        }

        super.onFBOUpdateEnd();

    }

在外部,我们如下使用我们的BitMap组件。

// this.missionContent  任务文字列表容器cc.Layeout

let bitmapCacheComp = this.missionContent.node.addComponent(BitmapCacheComponent); // 创建Bitmap组件

this.bitmapCacheComp._bitmapParent = this.bitmapLayer; // 指定父节点

然后在合适的时机(例如初始化或任务状态变化时)进行render。

this.bitmapCacheComp?.drawOnce();

以上就实现了N行文本合成一张texture来显示的功能,达到了合批的目的。draw也从N变成了1个。

当然,不仅是文本,可以render任意可渲染节点。

关联文章链接:

CocosCreator游戏性能优化(1):性能分析工具

CocosCreator游戏性能优化(3):GPU优化之降低计算分辨率

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,042评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,996评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,674评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,340评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,404评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,749评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,902评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,662评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,110评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,577评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,258评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,848评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,726评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,952评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,271评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,452评论 2 348

推荐阅读更多精彩内容