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