性能优化5-MutilTexture地图物件多图渲染合批

前言

在地图上常常有一大堆地图物件,起到丰富世界观、画面等作用,物件往往是需要和人物发生交互的(进行排序以避免渲染层级错误),但顺序发生改变后容易打断合批。如果能将图片塞到一个图集中,那问题也就解决了。

但如果不行呢?如果图集都塞满了呢?:no_mouth:

去年底的时候,由于希望在性能优化方面做一些研究,在论坛找到了江南百景图研发负责人的技术分享文章,其中提到:

《江南百景图》的资源非常多,每个玩家使用资源的顺序也不尽相同,如果玩家使用的资源分别在不同的图集上,还是会导致合批渲染被打断,产生 Draw Call。因此,针对这一情况,我们采用了 Multi-Texture 的方式进行了优化,其原理是将传统的判断是否在同一张图集,转换为判断是否在 同一批图集 ,这样就大大减少了 Draw Call 产生。

本文参照文章中的思路实现了这个优化。
根据分享文章中的描述,大部分手机可以支持8张图集。所以只要图集数量不超过8张,统统都只要1个dc

开发环境

浏览器:Chrome
开发语言:JavaScript
引擎版本:CocosCreator 2.4.3

词语缩写对照

分享文章:《江南百景图》的分享文章
textureIndex:地图对象的图集在Material中的index。

实现思路

其实做起来和性能优化4(Sprite颜色数据去除)有点像,这里不重复讲了,有需要可以回看一下嗷。
一样地,我们需要拓展渲染数据格式,增加存放textureIndex的位置,随后,将textureIndex传递给Material,最后根据textureIndex读取不同的图集就好了!

需求可拆分为如下实现步骤:

  1. 新建材质,实现根据textureIndex读取不同图集。
  2. 新建MutilTextureSprite及MutilTextureAssembler,支持textureIndex的设置及渲染数据填充。
  3. 新建MyObjectGroup,在创建物件时,将cc.Sprite改为MutilTextureSprite,收集出现的图集,并为MutilTextureSprite设置材质及textureIndex。
  4. 新建MyTiledMap,改变其默认创建的cc.TiledObjectGroup为MyObjectGroup。

涉及文件

文件名 说明
Game.js 游戏入口类,管控全局
MyTiledMap.js 自定义的地图类
MyObjectGroup.js 自定义的地图物件层类
MutilTextureSprite.js 实现MutilTexture的Sprite类
MutilTextureAssembler.js 实现MutilTexture的Assembler类
mutilTexture.mtl 实现MutilTexture的Material
mutilTexture.effect 实现MutilTexture的Effect

代码

类似的内容一样不赘述了。
先把effect代码写了。最大的改变是增加一堆texture。然后是对texture_idx进行判断,从不同图集中取color即可。代码里只写到texture5,实际可以写到7(即8张图集,但我实在是太懒了,只写到5做测试:laughing:)。

// mutilTexture.effect
CCEffect %{
  techniques:
  - passes:
    - vert: vs
      frag: fs
      blendState:
        targets:
        - blend: true
      rasterizerState:
        cullMode: none
      properties:
        texture: { value: white }
        texture1: { value: white }
        texture2: { value: white }
        texture3: { value: white }
        texture4: { value: white }
        texture5: { value: white }
        alphaThreshold: { value: 0.5 }
}%


CCProgram vs %{
  precision highp float;

  #include <cc-global>
  #include <cc-local>

  in vec3 a_position;
  in vec4 a_color;
  out vec4 v_color;
  
  in float a_texture_idx;
  out float texture_idx;

  #if USE_TEXTURE
  in vec2 a_uv0;
  out vec2 v_uv0;
  #endif

  void main () {
    texture_idx = a_texture_idx;
    vec4 pos = vec4(a_position, 1);

    #if CC_USE_MODEL
    pos = cc_matViewProj * cc_matWorld * pos;
    #else
    pos = cc_matViewProj * pos;
    #endif

    #if USE_TEXTURE
    v_uv0 = a_uv0;
    #endif

    v_color = a_color;

    gl_Position = pos;
  }
}%


CCProgram fs %{
  precision highp float;
  
  #include <alpha-test>
  #include <texture>

  in vec4 v_color;

  #if USE_TEXTURE
    in vec2 v_uv0;
    uniform sampler2D texture;
    uniform sampler2D texture1;
    uniform sampler2D texture2;
    uniform sampler2D texture3;
    uniform sampler2D texture4;
    uniform sampler2D texture5;
    
    in float texture_idx;
  #endif


  void main () {
    vec4 o = vec4(1, 1, 1, 1);

    #if USE_TEXTURE
      if (texture_idx <= 1.0) {
        CCTexture(texture, v_uv0, o);
      } else if (texture_idx <= 2.0) {
        CCTexture(texture1, v_uv0, o);
      } else if (texture_idx <= 3.0) {
        CCTexture(texture2, v_uv0, o);
      } else if (texture_idx <= 4.0) {
        CCTexture(texture3, v_uv0, o);
      } else if (texture_idx <= 5.0) {
        CCTexture(texture4, v_uv0, o);
      }
    #endif

    o *= v_color;

    ALPHA_TEST(o);

    gl_FragColor = o;
  }
}%

继承cc.Sprite实现设置textureIndex的函数,修改默认的assembler为MutilTextureAssembler

// MutilTextureSprite.js
import { MutilTextureAssembler } from "./MutilTextureAssembler";
	
cc.Class({
    extends: cc.Sprite,

    setTextureIdx (idx) {
        this._textureIdx = idx
        this.setVertsDirty();
    },

    _resetAssembler() {
        this.setVertsDirty();
        let assembler = this._assembler = new MutilTextureAssembler();

        this.setVertsDirty();

        assembler.init(this); 
        
        this._updateColor();
    },
});

设计新的顶点数据格式,容纳textureIndex数据。

// MutilTextureAssembler.js
let gfx = cc.gfx;
var vfmtPosUvColorIndex = new gfx.VertexFormat([
    { name: gfx.ATTR_POSITION, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },
    { name: gfx.ATTR_UV0, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },
    { name: "a_texture_idx", type: gfx.ATTR_TYPE_FLOAT32, num: 1 },
    { name: gfx.ATTR_COLOR, type: gfx.ATTR_TYPE_UINT8, num: 4, normalize: true },
]);

继承cc.Assembler,进行textureIndex的数据填充

// MutilTextureAssembler.js
export class MutilTextureAssembler extends cc.Assembler {
    // 增加updateTextureIdx的调用
    updateRenderData (sprite) {
        this.packToDynamicAtlas(sprite, sprite._spriteFrame);

        if (sprite._vertsDirty) {
            this.updateUVs(sprite);
            this.updateVerts(sprite);
            this.updateTextureIdx(sprite);
            sprite._vertsDirty = false;
        }
    },
    // 填充textureIndex数据
    updateTextureIdx(sprite) {
        let index = sprite._textureIdx;
        let verts = this._renderData.vDatas[0];
        
        verts[4] = index;
        verts[10] = index;
        verts[16] = index;
        verts[22] = index;
    }
}

增加MyObjectGroup。重写_init函数。实现创建MutilTextureSprite。在创建完后,为所有Sprite设置材质及textureIndex

// MyObjectGroup.js
cc.Class({
    extends: cc.TiledObjectGroup,
    _init (groupInfo, mapInfo, texGrids) {
        let spriteTextures = new Set();
        for (let i = 0, l = objects.length; i < l; i++) {
            if (objType === TMXObjectType.IMAGE) {
                let sp = imgNode.getComponent("MutilTextureSprite");
                if (!sp) {
                    sp = imgNode.addComponent("MutilTextureSprite");
                }
                // 收集所有图集
                spriteTextures.add(grid.tileset.sourceImage);
            }
        }
        this._objects = objects;
        
        // 加载自定义的材质
        cc.assetManager.loadAny({uuid:"b3O6UU0RhBwbCTJA60Njd0"}, cc.Material, undefined, (err, res)=>{
            // 设置材质的texture属性
            let textures = Array.from(spriteTextures);
            for (let i = 0; i < textures.length; i++) {
                let idx = i === 0 ? '' : i;
                res.setProperty(`texture${idx}`, textures[i], 0);
            }
            // 修改所有MutilTextureSprite的材质
            let children = this.node.children;
            for (let i = 0, n = children.length; i < n; i++) {
                let c = children[i];
                
                let sp = c.getComponent(cc.Sprite);
                sp.setMaterial(0, res);
                // 写死哈希值 使其可以合批
                sp.getMaterial(0).updateHash(9999);
                // 设置textureIndex
                let index = textures.indexOf(sp.spriteFrame._texture);
                sp.setTextureIdx(index + 1);
            }
        });
        
    }
});

最后最简单的,改Tiledmap。将cc.TiledObjectGroup改为MyObjectGroup

// MyTiledMap.js
cc.Class({
    extends: cc.TiledMap,

    _buildLayerAndGroup: function () {
      if (layerInfo instanceof cc.TMXObjectGroupInfo) {
          let group = child.getComponent("MyObjectGroup");
          if (!group) {
            group = child.addComponent("MyObjectGroup");
          }
          group._init(layerInfo, mapInfo, texGrids);
          groups.push(group);
      }
    }
}

需要注意的是,在测试的时候发现了一个bug,在模拟器中,物件的位置发生了改变
期望效果:
image
实际效果:
image
:sweat_smile:这就有点尴尬了。调整测试了几次,发现内容其实都渲染出来了,但是位置是错的

后来研究了一下,发现原生平台下,渲染数据中的顶点位置,计算方法发生了改变。所以我们需要对原生平台的情况做一些小处理。

// MutilTextureAssembler.js
updateWorldVerts (comp) {
    if (CC_NATIVERENDERER) {
        // 原生平台兼容代码 复制于jsb-engine.js中的cc.Assembler2D.prototype.updateWorldVerts
        var local = this._local;
        var verts = this._renderData.vDatas[0];
        var vl = local[0],
            vr = local[2],
            vb = local[1],
            vt = local[3];
        var floatsPerVert = this.floatsPerVert;
        var vertexOffset = 0; // left bottom
        
        verts[vertexOffset] = vl;
        verts[vertexOffset + 1] = vb;
        vertexOffset += floatsPerVert; // right bottom
        
        verts[vertexOffset] = vr;
        verts[vertexOffset + 1] = vb;
        vertexOffset += floatsPerVert; // left top
        
        verts[vertexOffset] = vl;
        verts[vertexOffset + 1] = vt;
        vertexOffset += floatsPerVert; // right top
        
        verts[vertexOffset] = vr;
        verts[vertexOffset + 1] = vt;
    } else {
        // 原本的代码
    }
}

效果对比

测试案例

有若干图集。0、1、2、3四个数字,分别属于4个图集。红色、绿色两个小炮塔,属于同一个图集。
物件数量…六千多个… 总之复制都复制了老半天… 为了作比较稍微夸张了点,同屏不可能有这么多物件吧:rofl:


渲染效果大概长这样子。DC数量中,包括资源监视器1次、监视器背景1次、显示DC的Label1次。所以优化后真的只有1次DC

上对比图:crazy_face:


数据是前200次渲染的耗时。横轴为次数,纵轴为耗时(单位ms)。
前一两次的渲染耗时比较高,导致图表看起来不太对劲。我们去掉前三次的耗时再看看。

优化效果还是明显的。

总结

明显看出,节省了大幅的渲染耗时。

本文的做法,在给MutilTextureSprite设置材质时,才进行了加载材质的动作,导致前若干帧还是使用默认的材质,会有点怪怪的。可以考虑把加载放到如开始游戏前,就可以和获取内置材质一样是同步的,这样直接重写_getDefaultMaterial就可以,效果会更好。

在研究的过程中,看到在江南百景图技术点一:MultiTexture实现文章中,大佬们讨论到effect代码中使用这么多if语句是否合适,有没有可能dc降低了,但渲染耗时反而更高。这个就需要大家在运用技术的时候做抉择了。

江南百景图相关的优化文章到这里就是最后一篇啦!第一次看到分享文章的时候,作为一个小菜鸡,这些优化思路真是天秀,长大见识啊。后来自己一点点找资料啃下来,也挺有趣的。

本系列也是最后一篇啦!写完的时候确实很爽,仿佛刚刚解决一个技术难题。总之研究的东西都分享给大家,个人水平有限,要是能对大家有一点点小帮助,那就太好了:grin:

非常感谢论坛各位大佬的无私分享,很多东西可能想很久都想不到。另外非常感谢@热心网友蒋先生,虽然我写的不怎么样但还是给我提供了许多意见。完结撒花:tada::tada::tada:

性能优化系列的其他文章(已经不是规划了哈哈哈哈哈哈):

  1. 列表层级渲染合批优化
  2. TiledMap地图优化-裁剪区域共享(Share Culling)
  3. 分帧寻路+寻路任务统一管理
  4. Sprite颜色数据去除
  5. 地图物件多图渲染合批(本文)

其中,除了列表渲染优化以外,都源于江南百景图的分享文章。

相关链接

DEMO源码 (1.1 MB)
如何重绘「江南百景图」?
江南百景图技术点一:MultiTexture实现

23赞

点赞 ,tiledMap 多图集渲染合批

2赞

===做一个更正===
实现的部分,收集图集和设置材质有一点问题。这会导致多物件层时,物件渲染错误。
原因:
我们把传递图集的代码写在了MyObjectGroup的init函数中,这导致解析每个图层的时候,都会重新传递一次图集列表。
如何更正:
把spriteTextures声明到MyTiledMap中,对应的传递图集代码也挪到MyTiledMap中。
详细步骤:

  1. 在MyTiledMap的_buildLayerAndGroup中,声明spriteTextures(set)。
  2. 在创建MyObjectGroup的时候,把spriteTextures当作参数传递
  3. 按旧逻辑往spriteTextures里增加图集、设置textureIndex。
  4. 在MyTiledMap的_buildLayerAndGroup中,使用spriteTextures传递图集给材质。

小优化:
维护spriteTextures的时候同时维护一个Map,key是图集,value是index。避免每次都通过indexOf获得index

2赞

这么好的文章不mark一个都对不起自己

mark一下

牛逼 ,tiledMap 多图集渲染合批

你的合批呢

mark一下

膜拜大佬!!

mark!!!

战略插眼 :call_me_hand:

mark!!!

多图集渲染 mark