【源码阅读】一张Sprite的渲染数据分析

一、抓个帧看看

二、数据哪里来?

现在,我们用上图抓帧的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) {
    },
};

好了。顶点数据有了。

那龙王怎么显示出来的呢?
image
在组装器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:

完。

大佬说学习要有方法,然后告诉我,费曼学习法,希望我梳理的内容大家能够了解一些吧。我在这过程中也受益良多。
很多讲的不足或者错误的地方,希望大佬们多指出,多关照。谢啦

14赞

详细到位,很赞

1赞

:+1: :+1: :+1: :+1:

1赞

Mark :grinning:

看了几遍,说实话收获很多,现在就是不知道MeshRender和MeshBuffer应该怎么理解,看了引擎源码这两个东西都废弃了,Mesh到底指的 “网格” 到底是指啥? :thinking:

我明白了,mesh一堆chunk,因为chunk是翻译为块,所以是"口",mesh翻译为网,所以是"田" :yum:

.已. 阅.

可以的,就是这种分析帖子才有意义

mark反复阅读