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优化之降低计算分辨率

你可能感兴趣的:(CocosCreator游戏性能优化(2):合批渲染之RenderToTarget)