egret UI 合批
图集方案
使用 Texture Merger
Egret 官方提供的图集工具,使用比较简单,打开工具,选择 Sprite Sheet
然后将要打图集的图片拖到窗口中,
然后会弹出新建项目确认框,输入名字,点击确定。
最后点 导出,导出合图文件,导出时,可以选择导出比例(100%、200%、50%):
导出后资源如下:
然后将导出的资源放到项目 Resource 目录下,将资源加到 default.res.json(这里注意,json 文件格式一定要选择 sheet 类型)
然后就可以在 UI 编辑器里使用图集里的资源了 111_json.00:
代码中使用图集:
// 下面的方法需要先在 default.res.json 中设置加载组 // 并且手动加载组中图集资源后,才能使用如下方式获取图集 // 加载整个图集 let ss: egret.SpriteSheet = RES.getRes("111_json") let tex: egret.Texture = ss.getTexture("00") let b1: egret.Bitmap = new egret.Bitmap(tex); // 通过二级 key 加载图集,如果有多张图集出现同名的子 key, // 返回最后加载的图集中的子 key 对应的贴图 let b2: egret.Bitmap = new egret.Bitmap(RES.getRes("00")); // 指定图集以及子键 let b3: egret.Bitmap = new egret.Bitmap(RES.getRes("111_json.00")); let b4: egret.Bitmap = new egret.Bitmap(RES.getRes("111_json#00"));
使用 FreeTexturePacker
官方自带的工具已经很好用了,不过有些时候,图集操作可以通过批处理命令,自带的工具不支持,因此选用其他方案。TextrurePacker 收费,这里选用免费的 FREE TEXTURE PACKER 1,按照文档上的操作,初步实现打出图集:
- 安装 nodejs
- 下载官方给的 demo,并解压
- 在 demo 目录打开命令窗口
- 执行: npm install -g grunt
- 执行: npm install
- 执行: grunt
最终图集生成在目录下的 dest 目录中:包含 一张图集 + 一个 json 文件,demo 中给的代码打出的图集 egret 编辑并不能识别,需要修改打包脚本 js 文件 2
module.exports = function(grunt) { grunt.initConfig({ free_tex_packer: { demo: { files: [ { expand: true, src: 'src/**/*', basePath: 'src/', filter: 'isFile' } ], options: { dest: 'dest', textureName: "atlas", width: 1024, height: 1024, fixedSize: false, padding: 0, allowRotation: true, detectIdentical: true, allowTrim: true, exporter: "Egret2D", // old: Pixi removeFileExtension: true, prependFolderName: true } } } }); grunt.loadNpmTasks('grunt-free-tex-packer'); grunt.registerTask('default', ['free_tex_packer']); };
改完后依旧不行,就去查看 egret 的图集格式 3
{ "file":"111.png", "frames":{ "00":{"x":364,"y":384,"w":68,"h":93,"offX":4, "offY":12,"sourceW":121,"sourceH":121}, "01":{"x":272,"y":461,"w":67,"h":93,"offX":9, "offY":13,"sourceW":121,"sourceH":121}, } }
修改成 Egret2D 后,demo 中打出的图集 json 不是 egret 格式
{ "file": "atlas.png", "frames": { "00": {"x": 0,"y": 0,"w": 121,"h": 121,"hw": 60.5,"hh": 60.5}, "01": {"x": 0,"y": 121,"w": 121,"h": 121,"hw": 60.5,"hh": 60.5}, } }
查看 free-tex-packer-core2 源码,可以看到导出模板:
// Egret2D.mst { "file": "{{config.imageName}}", "frames": { {{#rects}} "{{{name}}}": { "x": {{frame.x}}, "y": {{frame.y}}, "w": {{frame.w}}, "h": {{frame.h}}, "hw": {{frame.hw}}, "hh": {{frame.hh}} }{{^last}},{{/last}} {{/rects}} } }
因此参考文档使用自定义的导出模板:
// template.txt { "file": "{{config.imageName}}", "frames": { {{#rects}} "{{{name}}}": { "x": {{frame.x}}, "y": {{frame.y}}, "w": {{frame.w}}, "h": {{frame.h}}, "offX":{{spriteSourceSize.x}}, "offY":{{spriteSourceSize.y}}, "sourceW":{{spriteSourceSize.w}}, "sourceH":{{spriteSourceSize.h}} }{{^last}},{{/last}} {{/rects}} } }
参数:
x:小图的有效像素区域在大图中的起始坐标 x
y: 小图的有效像素区域在大图中的起始坐标 y
w: 小图的有效像素区域在大图中的宽度
h: 小图的有效像素区域在大图中的高度
offX:原始图片的左上角非透明区域的起始坐标 x (未开启 trim, offX = 0)
offY:原始图片的左上角非透明区域的起始坐标 y (未开启 trim, offY = 0)
sourceW:原始图片的宽度
sourceH:原始图片的高度 \
然后修改 grunt demo 代码,改成读取目录,分别创建对应的图集,当然也可以增加自定规则,将某几个目录打成一张图集,或者哪些目录不打图集:
String.prototype.format = function() { var formatted = this; for( var arg in arguments ) { formatted = formatted.replace("{" + arg + "}", arguments[arg]); } return formatted; }; let exporter = { fileExt: "json", template: "./template.txt", // 去除透明部分在合图 // 这个配置会覆盖 options 中的,一定要配置 allowTrim: true } function getOption(textureName) { let options = { dest: 'dest', textureName: textureName, fixedSize: false, padding: 1, allowRotation: true, detectIdentical: true, powerOfTwo: true, allowTrim: true, trimMode: "trim", packer: "MaxRectsPacker", exporter: exporter, removeFileExtension: true, prependFolderName: true } return options } function getAtlasInfo(src, atlasName) { let srcPath = "{0}/*".format(src) let basePath = "{0}/".format(src) let atlas = { files: [ {expand: true, src: srcPath, basePath: basePath, filter: 'isFile'}, ], options: getOption(atlasName) } return atlas } function getTexturePackConf(rootDir) { let packerConf = {} var fs = require("fs") var path = require("path") fs.readdirSync(rootDir, { withFileTypes: true}).forEach(function(dir) { var filePath = path.join(rootDir, dir.name) if(dir.isDirectory()) { packerConf[dir.name] = getAtlasInfo(filePath, dir.name) } }) return packerConf } module.exports = function(grunt) { let rootDir = "./src" rootDir = "E:/work/project/H5/ClockBloodUI/ClockTower/resource/ui_res" let packConf = getTexturePackConf(rootDir) grunt.initConfig({ free_tex_packer: packConf }); grunt.loadNpmTasks('grunt-free-tex-packer'); grunt.registerTask('default', ['free_tex_packer', ]); };
结果如下:
然后就是扫描已有的皮肤文件,将目前引用的贴图信息,修改成图集信息。
动态合图
KM 上看到一篇动态合图的文章,里面讲到网页版拉取小图会比拉取大图速度快,因此动态合图也是一个方案,不过 KM 上讲的技术点比较少,还需要看源码,下面是对 egret 渲染的源码分析:
egret 渲染流程
egret 全局渲染器
egret 有两个渲染器 systemRender 跟 canvasRender
namespace egret.sys { // 忽略下面坑爹的官方注释,web 端下渲染主要使用到的是 systemRender // WebGLRenderer 中有几处地方会使用到 canvasRenderer // 用于碰撞检测绘制 export let systemRenderer: SystemRenderer; // 显示渲染器接口 export let canvasRenderer: SystemRenderer; }
这两渲染器的基类都是 SystemRenderer
export interface SystemRenderer { render(displayObject: DisplayObject, buffer: RenderBuffer, matrix: Matrix, forRenderTexture?: boolean): number; drawNodeToBuffer(node: sys.RenderNode, buffer: RenderBuffer, matrix: Matrix, forHitTest?: boolean): void; renderClear(); }
egret 引擎代码入口在项目工程中的 index.html 中的 js 代码,在这里会根据当前设备信息来创建对应的渲染器(canvasRenderer 目前看源码只有渲染矢量节点,旧的文本渲染时用到):
- webgl: systemRender 就是 WebGLRenderer,canvasRenderer 是 Canvas 渲染器
- canvas: 这个时候,两个渲染器都是 Canvas 渲染器
// index.html egret.runEgret({ renderMode: "webgl"}); // src/egret\web\EgretWeb.ts function runEgret(options?: runEgretOptions): void { sys.CanvasRenderBuffer = CanvasRenderBuffer; setRenderMode(options.renderMode); } function setRenderMode(renderMode: string): void { if (renderMode == "webgl" && WebGLUtils.checkCanUseWebGL()) { sys.RenderBuffer = web.WebGLRenderBuffer; sys.systemRenderer = new WebGLRenderer(); sys.canvasRenderer = new CanvasRenderer(); sys.customHitTestBuffer = new WebGLRenderBuffer(3, 3); sys.canvasHitTestBuffer = new CanvasRenderBuffer(3, 3); Capabilities["renderMode" + ""] = "webgl"; } else { sys.RenderBuffer = web.CanvasRenderBuffer; sys.systemRenderer = new CanvasRenderer(); sys.canvasRenderer = sys.systemRenderer; sys.customHitTestBuffer = new CanvasRenderBuffer(3, 3); sys.canvasHitTestBuffer = sys.customHitTestBuffer; Capabilities["renderMode" + ""] = "canvas"; } }
egret 渲染 Player
创建完渲染器,还需要创建 Player,这个是直接渲染可见节点的对象,会直接获取 stage 上的 displayList,调用渲染函数 drawToSurface
// src\egret\player\Player.ts export class Player extends HashObject { public constructor(buffer: RenderBuffer, stage: Stage, entryClassName: string) { super(); this.stage = stage; this.screenDisplayList = this.createDisplayList(stage, buffer); } private createDisplayList(stage: Stage, buffer: RenderBuffer): DisplayList { let displayList = new DisplayList(stage); displayList.renderBuffer = buffer; stage.$displayList = displayList; return displayList; } $render(triggerByFrame: boolean, costTicker: number): void { if (egret.nativeRender) { egret_native.updateNativeRender(); egret_native.nrRender(); return; } if (egret.sys.systemRenderer.renderClear) { egret.sys.systemRenderer.renderClear(); } let stage = this.stage; let t1 = egret.getTimer(); let drawCalls = stage.$displayList.drawToSurface(); let t2 = egret.getTimer(); if (triggerByFrame && this.showFPS) { fpsDisplay.update(drawCalls, t2 - t1, costTicker); } } }
Player 创建也是在 runEgret 这个函数中,不过不是直接创建出 Player,而是创建网页节点解析对象 WebPlayer。我们在运行一个 egret 项目网页后,通过使用浏览器的检查功能,可以看到页面 body 中只有一个 div 标签,这个标签负责渲染整个游戏中的所有图元。
创建流程如下:
// src\egret\web\EgretWeb.ts function runEgret(options?: runEgretOptions): void { sys.CanvasRenderBuffer = CanvasRenderBuffer; setRenderMode(options.renderMode); // 创建 Player 对象 let list = document.querySelectorAll(".egret-player"); let length = list.length; for (let i = 0; i < length; i++) { let container = <HTMLDivElement>list[i]; let player = new WebPlayer(container, options); container["egret-player"] = player; } }
通过调试可以知道,egret 对 div 标签创建了一个 WebPlayer
在 WebPlayer 中又包含一个 Player 对象,这个 Player 对象负责渲染
export class WebPlayer extends egret.HashObject implements egret.sys.Screen { public constructor(container: HTMLDivElement, options: runEgretOptions) { super(); this.init(container, options); } private init(container: HTMLDivElement, options: runEgretOptions): void { console.log("Egret Engine Version:", egret.Capabilities.engineVersion) let option = this.readOption(container, options); let stage = new egret.Stage(); stage.$screen = this; let buffer = new sys.RenderBuffer(undefined, undefined, true); let player = new egret.sys.Player(buffer, stage, option.entryClassName); this.player = player; } }
DisplayList
Player 渲染调用的是 DisplayList 的接口,DisplayList 会从 root 节点开始渲染图元
export class DisplayList extends HashObject { private renderBuffer = new RenderBuffer(); public drawToSurface(): number { let drawCalls = 0; let buffer = this.renderBuffer; buffer.clear(); drawCalls = systemRenderer.render(this.root, buffer, this.offsetMatrix); } }
最后调用到 WebGLRenderer 中的渲染函数:
export class WebGLRenderer implements sys.SystemRenderer { public render(displayObject: DisplayObject, buffer: sys.RenderBuffer, matrix: Matrix, forRenderTexture?: boolean): number { this.nestLevel++; let webglBuffer: WebGLRenderBuffer = <WebGLRenderBuffer>buffer; let webglBufferContext: WebGLRenderContext = webglBuffer.context; let root: DisplayObject = forRenderTexture ? displayObject : null; webglBufferContext.pushBuffer(webglBuffer); //绘制显示对象 webglBuffer.transform(matrix.a, matrix.b, matrix.c, matrix.d, 0, 0); this.drawDisplayObject(displayObject, webglBuffer, matrix.tx, matrix.ty, true); webglBufferContext.$drawWebGL(); let drawCall = webglBuffer.$drawCalls; webglBuffer.onRenderFinish(); webglBufferContext.popBuffer(); let invert = Matrix.create(); matrix.$invertInto(invert); webglBuffer.transform(invert.a, invert.b, invert.c, invert.d, 0, 0); Matrix.release(invert); return drawCall; } }
从根节点开始渲染,进入 drawDisplayObject 函数,并逐一变量子节点生成渲染指令:
// cacheAsBitmap: 节点会有自己的 displayList // 并将节点渲染到一张贴图上 // WebGlRenderer.ts private drawDisplayObject(displayObject: DisplayObject, buffer: WebGLRenderBuffer, offsetX: number, offsetY: number, isStage?: boolean): number { // 忽略 cacheAsBitmap 的情况 // let displayList = displayObject.$displayList; let node: sys.RenderNode = displayObject.$getRenderNode() if(node) { switch (node.type) { case sys.RenderNodeType.BitmapNode: this.renderBitmap(<sys.BitmapNode>node, buffer); break; case sys.RenderNodeType.TextNode: this.renderText(<sys.TextNode>node, buffer); break; case sys.RenderNodeType.GraphicsNode: this.renderGraphics(<sys.GraphicsNode>node, buffer); break; case sys.RenderNodeType.GroupNode: this.renderGroup(<sys.GroupNode>node, buffer); break; case sys.RenderNodeType.MeshNode: this.renderMesh(<sys.MeshNode>node, buffer); break; case sys.RenderNodeType.NormalBitmapNode: this.renderNormalBitmap(<sys.NormalBitmapNode>node, buffer); break; } } let children = displayObject.$children; if (children) { if (displayObject.sortableChildren && displayObject.$sortDirty) { //绘制排序 按照 zIndex 排序 displayObject.sortChildren(); let length = children.length; for (let i = 0; i < length; i++) { let child = children[i]; switch (child.$renderMode) { case RenderMode.NONE: break; case RenderMode.FILTER: drawCalls += this.drawWithFilter(child, buffer, offsetX2, offsetY2); break; case RenderMode.CLIP: drawCalls += this.drawWithClip(child, buffer, offsetX2, offsetY2); break; case RenderMode.SCROLLRECT: drawCalls += this.drawWithScrollRect(child, buffer, offsetX2, offsetY2); break; default: drawCalls += this.drawDisplayObject(child, buffer, offsetX2, offsetY2); break; } } } }
顺道提一嘴,如果层级使用了 sortableChildren 跟 zIndex 的会出现事件层级跟渲染层级不一致的情况就是在这里
// src/egret/display/DisplayObjectContainer.ts $hitTest(stageX: number, stageY: number): DisplayObject { let found = false; let target: DisplayObject = null; // 事件响应没有对子节点进行排序,而是从下往上遍历 // 因此如果需要使用到 zIndex 这里需要做排序 for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; if (child.$maskedObject) { continue; } target = child.$hitTest(stageX, stageY); } }
渲染图片的函数主要就是 renderBitmap
private renderBitmap(node: sys.BitmapNode, buffer: WebGLRenderBuffer): void { buffer.context.drawImage(image, data[pos++], data[pos++], data[pos++], data[pos++], data[pos++], data[pos++], data[pos++], data[pos++], node.imageWidth, node.imageHeight, node.rotated, node.smoothing); } WebGLRenderContext.drawImage( image: BitmapData, sourceX: number, sourceY: number, sourceWidth: number, sourceHeight: number, destX: number, destY: number, destWidth: number, destHeight: number, imageSourceWidth: number, imageSourceHeight: number, rotated: boolean, smoothing?: boolean): void { let buffer = this.currentBuffer; this.drawTexture(texture, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight, imageSourceWidth, imageSourceHeight, undefined, undefined, undefined, undefined, rotated, smoothing); } WebGLRenderContext.drawTexture( texture: WebGLTexture, sourceX: number, sourceY: number, sourceWidth: number, sourceHeight: number, destX: number, destY: number, destWidth: number, destHeight: number, textureWidth: number, textureHeight: number, meshUVs?: number[], meshVertices?: number[], meshIndices?: number[], bounds?: Rectangle, rotated?: boolean, smoothing?: boolean): void { let buffer = this.currentBuffer; // 调用 $drawWebGL 绘制 if (meshVertices && meshIndices) { if (this.vao.reachMaxSize(meshVertices.length / 2, meshIndices.length)) { this.$drawWebGL(); } } else { if (this.vao.reachMaxSize()) { this.$drawWebGL(); } } // 往 this.drawData 推 drawData this.drawCmdManager.pushDrawTexture(texture, count, this.$filter, textureWidth, textureHeight); buffer.currentTexture = texture; // 增加顶点数据 this.vao.cacheArrays(buffer, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight, textureWidth, textureHeight, meshUVs, meshVertices, meshIndices, rotated) } public WebGLRenderContext.$drawWebGL() { let length = this.drawCmdManager.drawDataLen; let offset = 0; for (let i = 0; i < length; i++) { let data = this.drawCmdManager.drawData[i]; // 忽略上传 indicesArray 信息 this.drawData(data, 0); } }
最后是调用 gl 的地方
private drawData(data: any, offset: number) { let gl = this.context; let program: EgretWebGLProgram; switch (data.type) { case DRAWABLE_TYPE.TEXTURE: //这段的切换可以优化 filter 滤镜 后处理 ? // getProgram 获取顶点跟片源 shader if (filter) { if (filter.type === "custom") { program = EgretWebGLProgram.getProgram(gl, filter.$vertexSrc, filter.$fragmentSrc, filter.$shaderKey); } } else if (filter.type === "glow") { program = EgretWebGLProgram.getProgram(gl, EgretShaderLib.default_vert, EgretShaderLib.glow_frag, "glow"); } else { program = EgretWebGLProgram.getProgram(gl, EgretShaderLib.default_vert, EgretShaderLib.texture_frag, "texture"); } this.activeProgram(gl, program); this.syncUniforms(program, filter, data.textureWidth, data.textureHeight); offset += this.drawTextureElements(data, offset); } } // src/egret/web/WebSysImpl.ts function drawTextureElements(renderContext, data, offset) { var webglrendercontext = renderContext; var gl = webglrendercontext.context; gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, data.texture); var size = data.count * 3; gl.drawElements(gl.TRIANGLES, size, gl.UNSIGNED_SHORT, offset * 2); return size; }
了解完 egret 渲染流程后,我找到了 egret 论坛中的一篇帖子 4,主要思路是利用 egret 本身提供的 cacheBitMap 功能,渲染时:
- 创建一张大图 RenderTexture
- 将需要渲染的节点放到一个根节点上,并按照尺寸依次在大图上申请空间,并根据返回的位置设置控件在根节点上的坐标
- 使用 renderTexture 的方法,将这个根节点渲染到 RenderTexture 上,这样就生成了图集
- 图集生成完毕,替换掉渲染节点中的 Texture 信息将其指向大图即可。
// 收集渲染节点,并将其放置到根节点上 this.pack = new MaxRectsBinPack(this.maxSize, this.maxSize, false); for (var key in textMap) { var qlabelList = textMap[key].qlabelList; var textField = qlabelList[0].textField; textField.width += 2; textField.height += 2; var bounds = textField.getBounds(); // 使用切图算法,获取子节点在图集上的位置信息 var rect = this.pack.insert(bounds.width, bounds.height); if (!rect.width) { throw ("DSpriteSheet的尺寸" + this.maxSize + "溢出,请新建一个DSpriteSheet对象"); } // 根据切图算法返回的位置,设置节点位置 textField.x = rect.x; textField.y = rect.y; textMap[key].bounds = rect; this.container.addChild(textField); } if (!this.spriteTexture) { this.spriteTexture = new egret.RenderTexture(); } // 将布局好的节点全部渲染到 renderTexture 上 this.spriteTexture.drawToTexture(this.container, new egret.Rectangle(0, 0, this.maxSize, this.maxSize)); // 生成切图信息,方便从大图 renderTexture 生成小图的 Texture 信息 if (!this.spriteSheet) { this.spriteSheet = new egret.SpriteSheet(this.spriteTexture); } for (var key in textMap) { var qlabelList = textMap[key].qlabelList; for (var i = 0; i < qlabelList.length; i++) { var qlabel = qlabelList[i]; var bounds = textMap[key].bounds; var texture = this.spriteSheet.getTexture(key); // if (texture) { // qlabel.texture = texture; // } // else { // 从大图上获取对应小图的信息创建出该节点的贴图 // 并替换节点的贴图 qlabel.texture = this.spriteSheet.createTexture(key, Math.round(bounds.x), Math.round(this.maxSize - bounds.height - bounds.y), Math.round(bounds.width), Math.round(bounds.height) ); qlabel.textField.width -= 2; qlabel.textField.height -= 2; qlabel.onRender.call(qlabel); // } } }
参考这个思路,后续实现 Image 节点的动态合图。
参考资料
1. Free Texture Pakcer grunt module