egret_渲染源码阅读

记一次对egret性能优化 - drawcall , 先了解其渲染流程 和 合批 机制, 才能对症下药. 这里说的是 WebGL, 主要针对Texture合批
阅读之后发现其webgl渲染流程和 cocos 3.x 版本的渲染流程是极其相似的. 可以参考之前总结的cocos绘制流程: https://blog.csdn.net/yangxuan0261/article/details/49981347
egret GitHub 源码: https://github.com/egret-labs/egret-core


断点调试 绘制

如果了解 cocos 3.x 渲染流程的话, 知道其关键 合批 的操作是在 渲染命令中做的.
快速了解渲染流程的话, 可以设个断点, 通过调用栈了解 及其周边代码.
F12 展开调试面板, 在 /libs/modules/egret/egret.web.js 中断点

看其调用栈


源码阅读

绘制合批

上图是断点 渲染合批的关键方法, 渲染命令, 这里只看 贴图的合批, 渲染命名管理器类

1
2
3
4
5
6
7
8
9
10
11
12
export class WebGLDrawCmdManager {
...
public pushDrawTexture(texture:any, count:number = 2, filter?:any, textureWidth?:number, textureHeight?:number):void {
if(filter) {
// 目前有滤镜的情况下不会合并绘制, 这个filter会打断合批, 不止是滤镜, 颜色也会, 在egret修改颜色就是通过这个 filter 去修改的
...
} else {

if (this.drawDataLen == 0 || this.drawData[this.drawDataLen - 1].type != DRAWABLE_TYPE.TEXTURE || texture != this.drawData[this.drawDataLen - 1].texture || this.drawData[this.drawDataLen - 1].filter) { //这里是决定不合批的地方, 只要又一个条件不符合, 就会新建一个批次, 可以看到 相邻绘制命令 下贴图是同一张也是其中一个条件
...
}
}
  • 里面的 this.drawData 相当与 cocos 中的 renderqueue (没记错的话)

绘制

把绘制命令中的所有 data(批次) 执行一遍绘制

1
2
3
4
5
6
7
8
export class WebGLRenderContext {
...
public $drawWebGL() {
let length = this.drawCmdManager.drawDataLen;
let offset = 0;
for (let i = 0; i < length; i++) {
let data = this.drawCmdManager.drawData[i]; //合好批次的绘制命名(其实数据data), 并没有像 cocos 一样提供回调函数, 然用户自己写绘制的代码
offset = this.drawData(data, offset);

使用对应的 shader 绘制该批次

1
2
3
4
5
6
7
8
9
10
11
export class WebGLRenderContext {
...
private drawData(data: any, offset: number) {
...
switch (data.type) {
case DRAWABLE_TYPE.TEXTURE:
if (filter) {
if (filter.type === "custom") {
program = EgretWebGLProgram.getProgram(gl, filter.$vertexSrc, filter.$fragmentSrc, filter.$shaderKey); // 使用了 滤镜 等特殊效果的, 会用不同shader来绘制这一批次
...
offset += this.drawTextureElements(data, offset); // 执行一次绘制

提交绘制数据给图形显卡

1
2
3
4
5
6
7
private drawTextureElements(data: any, offset: number): number {
let gl: any = this.context;
gl.bindTexture(gl.TEXTURE_2D, data.texture);
let size = data.count * 3;
gl.drawElements(gl.TRIANGLES, size, gl.UNSIGNED_SHORT, offset * 2);
return size;
}

中断合批的各种操作

主要看 WebGLDrawCmdManager.tsthis.drawData 这个 绘制数据队列 会不会增加.

1
2
3
4
5
6
7
8
9
10
11
12
13
export const enum DRAWABLE_TYPE {
TEXTURE, // 除这个之外的所有操作都会造成合批中断
RECT, // 举行
PUSH_MASK, // 遮罩
POP_MASK,
BLEND, // 混合
RESIZE_TARGET,
CLEAR_COLOR,
ACT_BUFFER,
ENABLE_SCISSOR, // 剪裁
DISABLE_SCISSOR,
SMOOTHING
}

还有 修改颜色 (filter, 滤镜等效果), 系统字


修改颜色

会中断 Texture 的合批

1
2
3
4
5
6
7
export function setColorMatrix(obj: fairygui.GObject | egret.DisplayObject, matrix: number[]) {
let edo = obj instanceof fairygui.GObject ? obj.displayObject : obj;
let filters = (edo.filters || []).filter(f => !(f instanceof egret.ColorMatrixFilter));

filters.push(new egret.ColorMatrixFilter(matrix));
edo.filters = filters; // 这个filters 会中断合批, 所以对dc有要求的话,这个要慎用
}

系统字

会中断 Texture 的合批, 且相邻节点的 系统字并不会合批, 也就是说 每一个系统字节点都是一个批次, 其会打断 Texture 的合批

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class WebGLRenderer implements sys.SystemRenderer {
...
private renderText(node: sys.TextNode, buffer: WebGLRenderBuffer): void {
...
// 拷贝canvas到texture
let texture = node.$texture;
if (!texture) {
texture = buffer.context.createTexture(<BitmapData><any>surface);
node.$texture = texture;
} else {
// 重新拷贝新的图像
buffer.context.updateTexture(texture, <BitmapData><any>surface);
}
...
buffer.context.drawTexture(node.$texture, 0, 0, textureWidth, textureHeight, 0, 0, textureWidth / canvasScaleX, textureHeight / canvasScaleY, textureWidth, textureHeight);