一、抓个帧看看
二、数据哪里来?
现在,我们用上图抓帧的sprite(simple模式)来追踪!
2.1顶点数据
在源码static-vb-accessor.ts中
StaticVBChunk中封装了顶点数据、索引数据
// 用于渲染的数据块
export class StaticVBChunk {
private _ib: Uint16Array; //给native用的index buffer数据
public vertexAccessor: StaticVBAccessor, // 管理和分配buffer,bufferId为key
public bufferId: number,
public meshBuffer: MeshBuffer,
public vertexOffset: number, // 顶点偏移=9
public vb: Float32Array, // 顶点数据([xyzuvrgba]*4)
public indexCount: number, // 索引数量 sprite中的simple渲染为6个点, 两个三角面组成的矩形
}
// 管理和分配一个大的buffer数据块
export class BufferAccessor {
public get attributes (): Readonly<Attribute[]> { return this._attributes; }
public get vertexFormatBytes () { return this._vertexFormatBytes; }
public get floatsPerVertex () { return this._floatsPerVertex; }
protected _device: Device = null!
protected _attributes: Attribute[] = null!; // xyz,uv,rgba
protected _vertexFormatBytes: number; // 一个顶点格式的byte数量:9 * 4 = 36,(4为float32的byte数量)
protected _floatsPerVertex: number; // 一个顶点数据,9个float32
protected _buffers: MeshBuffer[] = []; // 管理多个MeshBuffer 2d网格数据buffer
}
// construct会分配一块mesh buffer
class StaticVBAccessor extends BufferAccessor {
private _freeLists: IFreeEntry[][] = [];
private _vCount = 0;
private _iCount = 0;
private _id = 0;
public constructor (device: Device, attributes: Attribute[], vCount?: number, iCount?: number) {
super(device, attributes);
// 最大顶点容量
// 默认是4096个可以在设置那里修改
// 说是超过后会扩容,但是我没找到扩容的代码,我估计是剩余容量不足,只能到另外一块buffer,新走一个batch
this._vCount = vCount || Math.floor(macro.BATCHER2D_MEM_INCREMENT * 1024 / this._vertexFormatBytes);
// 最大索引容量, 4096 * 4(一个矩形4个索引点)
this._iCount = iCount || (this._vCount * StaticVBAccessor.IB_SCALE);
this._id = StaticVBAccessor.generateID();
// Initialize first mesh buffer
// 分配第一块mesh buffer
this._allocateBuffer();
}
}
MeshBuffer中存着2d 渲染使用的网格缓冲数据
export class MeshBuffer {
protected _byteOffset = 0; // 字节偏移量
protected _vertexOffset = 0; // 顶点数偏移
protected _indexOffset = 0; // 索引偏移
protected _floatsPerVertex = 0; // 每顶点的float长度
protected _vData: Float32Array = null!; // 顶点数据
protected _iData: Float32Array = null!; // 索引数据
private _vertexFormatBytes = 0; // 顶点格式的字节数
private _initVDataCount = 0; // vData初始大小 4096 * 9
private _initIDataCount = 0; // iData初始大小 4096 * 4
private _attributes: Attribute[] = null!;
private _attributes: Attribute[] = null!; // buffer 的顶点属性
protected _dirty = false; // 脏标记
private _iaPool: IIARef[] = []; // 数据在同一块buffer中,避免频繁分配。IIARef记录IA数据在bufer中的位置信息
}
以上是引擎对于的封装,大概如下图吧
Accssor相当于大地主,有很多块田(MeshBuffer),然后StaticVBchunk是用了田的某一部分(口?口?口?口)
那耕田佬是谁?–Assembler。
看看sprite的simple Assembler
// StaticVBAccessor中分配一个chunk
public allocateChunk (vertexCount: number, indexCount: number) {
const byteLength = vertexCount * this.vertexFormatBytes;
let buf: MeshBuffer = null!; let freeList: IFreeEntry[];
let bid = 0; let eid = -1; let entry: IFreeEntry | null = null;
//... 略
if (entry) {
// 计算在meshbuffer中的偏移
const vertexOffset = entry.offset / this.vertexFormatBytes;
// 需要注意的是vb是从buffer中截取所需的部分,然后全部填充0
const vb = new Float32Array(buf.vData.buffer, entry.offset, byteLength >> 2).fill(0);
this._allocateChunkFromEntry(bid, eid, entry, byteLength);
// 返回chunk
return new StaticVBChunk(this, bid, buf, vertexOffset, vb, indexCount);
}
}
/**
* simple 组装器
* 可通过 `UI.simple` 获取该组装器。
* 世界坐标顶点数据 xyz
* uv坐标数据 xy
* 颜色数据 rgba
* 一个顶点数据共9个float,一个矩形4个顶点36个float 填入到meshbuffer.vData
* 一个矩形索引是012,132,逆时针组成的两个三角形面 填入到meshbuffer.iData
*/
export const simple: IAssembler = {
createData (sprite: Sprite) {
const renderData = sprite.requestRenderData(); // 渲染所需的数据对象
renderData.dataLength = 4; // 4 vertices 四个角的顶点
renderData.resize(4, 6); // 里面会分配一个chunk, 4个顶点的数据(4*9个float), 6索引
renderData.vertexRow = 2; // 2 floats per vertex
renderData.vertexCol = 2; // 2 floats per vertex
renderData.chunk.setIndexBuffer(QUAD_INDICES); // 绘制一个矩形固定的6个索引012132
return renderData;
},
// 更新数据
updateRenderData (sprite: Sprite) {
},
// 更新世界坐标系下的顶点位置
updateWorldVerts (sprite: Sprite, chunk: StaticVBChunk) {
const renderData = sprite.renderData!;
// 把数据填入到chunk的vb
const vData = chunk.vb;
// 略
vData[offset + 0] = (m.m00 * x + m.m04 * y + m.m12) * rhw;
vData[offset + 1] = (m.m01 * x + m.m05 * y + m.m13) * rhw;
vData[offset + 2] = (m.m02 * x + m.m06 * y + m.m14) * rhw;
},
// 把索引数据填入到ib
fillBuffers (sprite: Sprite, renderer: IBatcher) {
const renderData = sprite.renderData!;
const chunk = renderData.chunk;
const ib = chunk.meshBuffer.iData;
let indexOffset = meshBuffer.indexOffset;
for (let curRow = 0; curRow < renderData.vertexRow - 1; curRow++) {
for (let curCol = 0; curCol < renderData.vertexCol - 1; curCol++) {
// vid is the index of the left bottom vertex in each rect.
const vid = vidOrigin + curRow * renderData.vertexCol + curCol;
// left bottom
ib[indexOffset++] = vid;
// 略
// 索引数据下标移动
meshBuffer.indexOffset += 6;
}
}
},
// 更新本地顶点数据
updateVertexData (sprite: Sprite) {
},
// 更新UV,存储到vb
updateUVs (sprite: Sprite) {
},
// 更新color,存储到vb
updateColor (sprite: Sprite) {
},
};
好了。顶点数据有了。
那龙王怎么显示出来的呢?

在组装器simple中的createData方法中,会取一个RenderData,其中就包含其他的渲染数据。
// ui-renderer.ts Sprite继承UIRenderer
/**
* @zh 请求新的渲染数据对象。
*/
public requestRenderData (drawInfoType = RenderDrawInfoType.COMP) {
const data = RenderData.add();
data.initRenderDrawInfo(this, drawInfoType);
this._renderData = data;
return data;
}
sprite的renderData中有spriteFrame的引用,龙王在这里。
我们日常经常碰到的这些数据大概就这些了。
接下来打算梳理下sprite-simple的渲染流程,其实Yip老大已经讲过了:https://forum.cocos.org/t/topic/139272。
1. Sprite继承UIRenderer,其中UIRenderer中包含一个RenderData对象
sprite继承UIRenderer,其中UIRenderer中包含一个RenderData对象
RenderData中包含了渲染需要的数据
material
texture
frame
chunk
_vc
_ic
stride
floatStride
vertexFormat
_data: IRenderData[] = [];
2. 获取renderdata,chunk对象
//// ui-renderer.ts
public __preload () {
this.node._uiProps.uiComp = this;
if (this._flushAssembler) {
this._flushAssembler();
}
}
//// sprite.ts
// 加载sprite
public __preload () {
this.changeMaterialForDefine();
super.__preload();
}
protected _flushAssembler () {
// 此处根据spriteAssembler.getAssembler方法根据type获取组装器,默认是simple
const assembler = Sprite.Assembler.getAssembler(this);
// 所以这里是simple.createData
this._renderData = this._assembler.createData(this);
// 材质,默认是ui-sprite-material
this.renderData!.material = this.getRenderMaterial(0);
this.markForUpdateRenderData();
// UV
this._assembler.updateUVs(this);
// RGBA
this._updateColor();
}
3. 填充chunk
assembler中处理填充逻辑,顶点为xyzuvrgba格式
sprite的simple组装器(vfmtPosUvColor)
updateVertexData,存储顶点数据,本地坐标
updateWorldVerts填充顶点数据,世界坐标3个float
updateUVs填充uv数据,2个float
updateColor填充color数据,4个float
fillBuffers填充索引数据,6个float
4. update数据
Batcher2D.commitComp->Batcher2D.uploadBuffers->StaticVBAccessor.uploadBuffers->MeshBuffer.uploadBuffers
//// mesh-buffer.ts
// iaf发挥了作用
public uploadBuffers () {
const byteCount = this.byteOffset;
const indexCount = this.indexOffset;
for (let i = 0; i < submitCount; ++i) {
const iaRef = this._iaPool[i];
const verticesData = new Float32Array(this.vData.buffer, 0, byteCount >> 2);
const indicesData = new Uint16Array(this.iData.buffer, 0, indexCount);
const vertexBuffer = iaRef.vertexBuffers[0];
if (byteCount > vertexBuffer.size) {
vertexBuffer.resize(byteCount);
}
// 更新vertex buffer
vertexBuffer.update(verticesData);
if (indexCount * 2 > iaRef.indexBuffer.size) {
iaRef.indexBuffer.resize(indexCount * 2);
}
// 更新index buffer
iaRef.indexBuffer.update(indicesData);
}
this.dirty = false;
}
在webgl中更新buffer的方法
WebGLCmdFuncUpdateBuffer(
WebGLDeviceManager.instance,
this._gpuBuffer!,
buffer as BufferSource,
0,
buffSize,
);
gl中
gl.bindBuffer(gl.ARRAY_BUFFER, gpuBuffer.glBuffer);
gl.bufferSubData(gpuBuffer.glTarget, offset, buff);
顶点数据:
![image.png]
索引数据:
![image.png]
不过这个时候还没到gpu,因为还没drawcall
5. 绘制
walk root之后,batch信息收集完毕。在Root._frameMoveEnd中处理batches的提交
//// root.ts
private _frameMoveEnd () {
const { director, Director } = cclegacy;
const cameraList = this._cameraList;
if (this._pipeline && cameraList.length > 0) {
director.emit(Director.EVENT_BEFORE_COMMIT);
cameraList.sort((a: Camera, b: Camera) => a.priority - b.priority);
for (let i = 0; i < cameraList.length; ++i) {
cameraList[i].geometryRenderer?.update();
}
director.emit(Director.EVENT_BEFORE_RENDER);
// 处理管线的render逻辑
this._pipeline.render(cameraList);
director.emit(Director.EVENT_AFTER_RENDER);
this._device.present();
}
if (this._batcher) this._batcher.reset();
}
默认是前向渲染管线forward-pipeline.ts
//// forward-pipeline.ts
public render (cameras: Camera[]) {
this.emit(PipelineEventType.RENDER_FRAME_BEGIN, cameras);
for (let i = 0; i < cameras.length; i++) {
const camera = cameras[i];
if (camera.scene) {
this.emit(PipelineEventType.RENDER_CAMERA_BEGIN, camera);
for (let j = 0; j < this._flows.length; j++) {
//execute render flow
this._flows[j].render(camera);
}
this.emit(PipelineEventType.RENDER_CAMERA_END, camera);
}
}
this.emit(PipelineEventType.RENDER_FRAME_END, cameras);
this._commandBuffers[0].end();
this._device.queue.submit(this._commandBuffers);
}
因为是2d渲染,只看forward-flow的render逻辑
forward-flow.render()->render-flow.render()->forward-stage.render()
//// forward-stage.ts
public render (camera: Camera) {
// 略
this._uiPhase.render(camera, renderPass);
}
//// ui-phase.ts
// 这里取出batches
public render (camera: Camera, renderPass: RenderPass) {
const batches = scene.batches;
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
const count = batch.shaders.length;
for (let j = 0; j < count; j++) {
const pass = batch.passes[j];
if (pass.phase !== this._phaseID) continue;
// shaderpass的attributes,block,tex,texsampler(多纹理合批估计会有多个)
const pso = PipelineStateManager.getOrCreatePipelineState(device, pass, shader, renderPass, inputAssembler);
cmdBuff.bindPipelineState(pso);
const inputAssembler = batch.inputAssembler!;
cmdBuff.bindInputAssembler(inputAssembler);
cmdBuff.draw(inputAssembler);
}
}
}
准备绘制
//// webgl-primary-command-buffer.ts
cmdBuff.draw(inputAssembler);
WebGLCommandBuffer
bindStates
6.1 设置shader程序和渲染状态(cull,depth,blend)
gl.useProgram(glProgram);
设置cullMode
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);
depth-stencil state
6.2 设置block参数(uniforms)
gl.uniform1iv
gl.uniform2iv
等等
6.3 采样器设置 sampler(设置texture和采样方式)
取出texture的单位,在gpuShader.glSamplerTextures里面
gl.activeTexture
gl.bindTexture
gl.texParameteri(gpuTexture.glTarget, gl.TEXTURE_WRAP_S, glWrapS);
gl.texParameteri(gpuTexture.glTarget, gl.TEXTURE_WRAP_T, glWrapT);
6.4 绑定buffer(vertex/index)
use VAO
vao.bindVertexArrayOES(glVAO);
非VAO
绑定buffer
gl.bindBuffer
设置buffer数据
gl.enableVertexAttribArray
设置数据读取方式
gl.vertexAttribPointer
draw
gl.drawElements(glPrimitive, drawInfo.indexCount, gpuInputAssembler.glIndexType, offset);
DrawCall + 1
++this._numDrawCalls;
附录补充-创建texture:
附录补充-图片文件到spriteFrame:
完。
大佬说学习要有方法,然后告诉我,费曼学习法,希望我梳理的内容大家能够了解一些吧。我在这过程中也受益良多。
很多讲的不足或者错误的地方,希望大佬们多指出,多关照。谢啦












