前言
在地图上常常有一大堆地图物件,起到丰富世界观、画面等作用,物件往往是需要和人物发生交互的(进行排序以避免渲染层级错误),但顺序发生改变后容易打断合批。如果能将图片塞到一个图集中,那问题也就解决了。
但如果不行呢?如果图集都塞满了呢?
去年底的时候,由于希望在性能优化方面做一些研究,在论坛找到了江南百景图研发负责人的技术分享文章,其中提到:
《江南百景图》的资源非常多,每个玩家使用资源的顺序也不尽相同,如果玩家使用的资源分别在不同的图集上,还是会导致合批渲染被打断,产生 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读取不同的图集就好了!
需求可拆分为如下实现步骤:
- 新建材质,实现根据textureIndex读取不同图集。
- 新建MutilTextureSprite及MutilTextureAssembler,支持textureIndex的设置及渲染数据填充。
- 新建MyObjectGroup,在创建物件时,将cc.Sprite改为MutilTextureSprite,收集出现的图集,并为MutilTextureSprite设置材质及textureIndex。
- 新建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做测试)。
// 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,在模拟器中,物件的位置发生了改变。
期望效果:
实际效果:
这就有点尴尬了。调整测试了几次,发现内容其实都渲染出来了,但是位置是错的。
后来研究了一下,发现原生平台下,渲染数据中的顶点位置,计算方法发生了改变。所以我们需要对原生平台的情况做一些小处理。
// 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个图集。红色、绿色两个小炮塔,属于同一个图集。
物件数量…六千多个… 总之复制都复制了老半天… 为了作比较稍微夸张了点,同屏不可能有这么多物件吧
渲染效果大概长这样子。DC数量中,包括资源监视器1次、监视器背景1次、显示DC的Label1次。所以优化后真的只有1次DC。
上对比图
数据是前200次渲染的耗时。横轴为次数,纵轴为耗时(单位ms)。
前一两次的渲染耗时比较高,导致图表看起来不太对劲。我们去掉前三次的耗时再看看。
优化效果还是明显的。
总结
明显看出,节省了大幅的渲染耗时。
本文的做法,在给MutilTextureSprite设置材质时,才进行了加载材质的动作,导致前若干帧还是使用默认的材质,会有点怪怪的。可以考虑把加载放到如开始游戏前,就可以和获取内置材质一样是同步的,这样直接重写_getDefaultMaterial就可以,效果会更好。
在研究的过程中,看到在江南百景图技术点一:MultiTexture实现文章中,大佬们讨论到effect代码中使用这么多if语句是否合适,有没有可能dc降低了,但渲染耗时反而更高。这个就需要大家在运用技术的时候做抉择了。
江南百景图相关的优化文章到这里就是最后一篇啦!第一次看到分享文章的时候,作为一个小菜鸡,这些优化思路真是天秀,长大见识啊。后来自己一点点找资料啃下来,也挺有趣的。
本系列也是最后一篇啦!写完的时候确实很爽,仿佛刚刚解决一个技术难题。总之研究的东西都分享给大家,个人水平有限,要是能对大家有一点点小帮助,那就太好了。
非常感谢论坛各位大佬的无私分享,很多东西可能想很久都想不到。另外非常感谢@热心网友蒋先生,虽然我写的不怎么样但还是给我提供了许多意见。完结撒花
性能优化系列的其他文章(已经不是规划了哈哈哈哈哈哈):
- 列表层级渲染合批优化
- TiledMap地图优化-裁剪区域共享(Share Culling)
- 分帧寻路+寻路任务统一管理
- Sprite颜色数据去除
- 地图物件多图渲染合批(本文)
其中,除了列表渲染优化以外,都源于江南百景图的分享文章。
相关链接
DEMO源码 (1.1 MB)
如何重绘「江南百景图」?
江南百景图技术点一:MultiTexture实现