【v3.6.0源码解读】从Web到Native,浅谈2D渲染流程

v3.6.0出来后,官方宣传2D原生性能大幅提升,原因是在Native上将很多代码放到C++层去实现了,正好最近我自己看了一遍2D渲染相关的源码,梳理了流程,在此分享一下。

PS: 目前关于自定义渲染的内容,在3.6版本中官方还没有放出像以前2.x那样方便的接口供开发者使用,如果有自定义顶点数据、渲染组件的需求可以参考我这个项目(支持Web与Native):【CocosTextMeshPro】一个文本渲染解决方案——支持字体颜色渐变、斜体、下划线、删除线、描边、镂空、阴影、辉光、顶点动画、新的排版模式

阅读须知

  • 本文会大量引用引擎源码,每个代码段注释处都会标明源码文件名
  • 水平有限,如有理解错误的地方,希望能够指正

Web与Native共用部分

主循环

  1. tick函数即引擎的主循环,可以看到各个系统的更新都在此处
  2. uiRendererManager.updateAllDirtyRenderers(); 是所有ui渲染组件顶点数据组装的入口
  3. this._root!.frameMove(dt); 是每帧执行渲染流程的入口函数
// director.ts
public tick (dt: number) {
    if (!this._invalid) {
        this.emit(Director.EVENT_BEGIN_FRAME);
        if (!EDITOR || legacyCC.GAME_VIEW) {
            // @ts-expect-error _frameDispatchEvents is a private method.
            input._frameDispatchEvents();
        }
        // Update
        if (!this._paused) {
            this.emit(Director.EVENT_BEFORE_UPDATE);
            // Call start for new added components
            this._compScheduler.startPhase();
            // Update for components
            this._compScheduler.updatePhase(dt);
            // Update systems
            for (let i = 0; i < this._systems.length; ++i) {
                this._systems[i].update(dt);
            }
            // Late update for components
            this._compScheduler.lateUpdatePhase(dt);
            // User can use this event to do things after update
            this.emit(Director.EVENT_AFTER_UPDATE);
            // Destroy entities that have been removed recently
            CCObject._deferredDestroy();
            // Post update systems
            for (let i = 0; i < this._systems.length; ++i) {
                this._systems[i].postUpdate(dt);
            }
        }

        this.emit(Director.EVENT_BEFORE_DRAW);
        uiRendererManager.updateAllDirtyRenderers();
        this._root!.frameMove(dt);
        this.emit(Director.EVENT_AFTER_DRAW);

        Node.resetHasChangedFlags();
        Node.clearNodeArray();
        containerManager.update(dt);
        this.emit(Director.EVENT_END_FRAME);
        this._totalFrames++;
    }
}

UIRenderer是所有ui渲染组件的基类,在this._assembler.updateRenderData(this)中负责准备后续需要的顶点数据,各个渲染组件具体实现不同,此处是自定义渲染所要处理的其中一个关键点

// ui-renderer.ts
public updateRenderer () {
    if (this._assembler) {
        this._assembler.updateRenderData(this);
    }
    this._renderFlag = this._canRender();
    this._renderEntity.enabled = this._renderFlag;
}

以sprite组件simple类型为例

  1. this.updateUVs(sprite) 直接更新了uv数据,填充进顶点缓冲(VBO)中
  2. this.updateVertexData(sprite) 更新了局部顶点坐标,但仅缓存到renderData.data中,而不是直接填充进顶点缓冲(VBO)中。因为此处的顶点坐标需要转换成世界顶点坐标,再传入顶点着色器中,而这个转换过程是由的root.ts中的frameMove函数实现的。(注:此处实现Native是在C++层实现的)
// simple.ts
updateRenderData (sprite: Sprite) {
    const frame = sprite.spriteFrame;
    dynamicAtlasManager.packToDynamicAtlas(sprite, frame);
    this.updateUVs(sprite);// dirty need
    //this.updateColor(sprite);// dirty need
    const renderData = sprite.renderData;
    if (renderData && frame) {
        if (renderData.vertDirty) {
            this.updateVertexData(sprite);
        }
        renderData.updateRenderData(sprite, frame);
    }
}

updateUVs (sprite: Sprite) {
    if (!sprite.spriteFrame) return;
    const renderData = sprite.renderData!;
    const vData = renderData.chunk.vb;
    const uv = sprite.spriteFrame.uv;
    vData[3] = uv[0];
    vData[4] = uv[1];
    vData[12] = uv[2];
    vData[13] = uv[3];
    vData[21] = uv[4];
    vData[22] = uv[5];
    vData[30] = uv[6];
    vData[31] = uv[7];
}

updateVertexData (sprite: Sprite) {
    const renderData: RenderData | null = sprite.renderData;
    if (!renderData) {
        return;
    }

    const uiTrans = sprite.node._uiProps.uiTransformComp!;
    const dataList: IRenderData[] = renderData.data;
    const cw = uiTrans.width;
    const ch = uiTrans.height;
    const appX = uiTrans.anchorX * cw;
    const appY = uiTrans.anchorY * ch;
    let l = 0;
    let b = 0;
    let r = 0;
    let t = 0;

    if (sprite.trim) {
        l = -appX;
        b = -appY;
        r = cw - appX;
        t = ch - appY;
    } else {
        const frame = sprite.spriteFrame!;
        const originSize = frame.originalSize;
        const ow = originSize.width;
        const oh = originSize.height;
        const scaleX = cw / ow;
        const scaleY = ch / oh;
        const trimmedBorder = frame.trimmedBorder;
        l = trimmedBorder.x * scaleX - appX;
        b = trimmedBorder.z * scaleY - appY;
        r = cw + trimmedBorder.y * scaleX - appX;
        t = ch + trimmedBorder.w * scaleY - appY;
    }
    dataList[0].x = l;
    dataList[0].y = b;
    dataList[1].x = r;
    dataList[1].y = b;
    dataList[2].x = l;
    dataList[2].y = t;
    dataList[3].x = r;
    dataList[3].y = t;
    renderData.vertDirty = true;
}

assembler.updateRenderData会调用renderData.updateRenderData(sprite, frame);

  1. this._renderDrawInfo上的数据会绑定到C++层
  2. this._renderDrawInfo.fillRender2dBuffer(this._data) 注意这行代码,继续往下看
// render-data.ts
public updateRenderData (comp: UIRenderer, frame: SpriteFrame | TextureBase) {
    if (this.passDirty) {
        this.material = comp.getRenderMaterial(0)!;
        this.passDirty = false;
        this.hashDirty = true;
        if (this._renderDrawInfo) {
            this._renderDrawInfo.setMaterial(this.material);
        }
    }
    if (this.nodeDirty) {
        const renderScene = comp.node.scene ? comp._getRenderScene() : null;
        this.layer = comp.node.layer;
        // Hack for updateRenderData when node not add to scene
        if (renderScene !== null) {
            this.nodeDirty = false;
        }
        this.hashDirty = true;
    }
    if (this.textureDirty) {
        this.frame = frame;
        this.textureHash = frame.getHash();
        this.textureDirty = false;
        this.hashDirty = true;
        if (this._renderDrawInfo) {
            this._renderDrawInfo.setTexture(this.frame ? this.frame.getGFXTexture() : null);
            this._renderDrawInfo.setSampler(this.frame ? this.frame.getGFXSampler() : null);
        }
    }
    if (this.hashDirty) {
        this.updateHash();
        if (this._renderDrawInfo) {
            this._renderDrawInfo.setDataHash(this.dataHash);
        }
    }

    // Hack Do not update pre frame
    if (JSB && this.multiOwner === false) {
        if (DEBUG) {
            assert(this._renderDrawInfo.render2dBuffer.length === this._floatStride * this._data.length, 'Vertex count doesn\'t match.');
        }
        // sync shared buffer to native
        this._renderDrawInfo.fillRender2dBuffer(this._data);
    }
}

如下所示,为render-draw-info.ts中的代码

  1. 准备渲染数据的时候会调用initRender2dBuffer,作用是让C++层持有JS层this._render2dBuffer这个对象的指针
  2. fillRender2dBuffer中负责将局部坐标数据填充进this._render2dBuffer,由于前面已经在C++层持有指向此JS对象的指针,故后续C++层可直接通过指针访问JS层填充的局部坐标数据
// render-draw-info.ts
public initRender2dBuffer () {
    if (JSB) {
        this._render2dBuffer = new Float32Array(this._vbCount * this._stride);
        this._nativeObj.setRender2dBufferToNative(this._render2dBuffer);
    }
}

public fillRender2dBuffer (vertexDataArr: IRenderData[]) {
    if (JSB) {
        const fillLength = Math.min(this._vbCount, vertexDataArr.length);
        let bufferOffset = 0;
        for (let i = 0; i < fillLength; i++) {
            const temp = vertexDataArr[i];
            this._render2dBuffer[bufferOffset] = temp.x;
            this._render2dBuffer[bufferOffset + 1] = temp.y;
            this._render2dBuffer[bufferOffset + 2] = temp.z;
            bufferOffset += this._stride;
        }
    }
}

Web渲染流程

  1. frameMove函数中首先计算了fps
  2. this._batcher.update(); 这行代码是2d渲染的重点,负责ui每帧渲染数据的处理
// root.ts
public frameMove (deltaTime: number) {
    this._frameTime = deltaTime;
    ++this._frameCount;
    this._cumulativeTime += deltaTime;
    this._fpsTime += deltaTime;
    if (this._fpsTime > 1.0) {
        this._fps = this._frameCount;
        this._frameCount = 0;
        this._fpsTime = 0.0;
    }
    for (let i = 0; i < this._scenes.length; ++i) {
        this._scenes[i].removeBatches();
    }
    const windows = this._windows;
    const cameraList: Camera[] = [];
    for (let i = 0; i < windows.length; i++) {
        const window = windows[i];
        window.extractRenderCameras(cameraList);
    }

    if (this._pipeline && cameraList.length > 0) {
        this._device.acquire([deviceManager.swapchain]);
        const scenes = this._scenes;
        const stamp = legacyCC.director.getTotalFrames();
        if (this._batcher) {
            this._batcher.update();
            this._batcher.uploadBuffers();
        }
        for (let i = 0; i < scenes.length; i++) {
            scenes[i].update(stamp);
        }

        legacyCC.director.emit(legacyCC.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();
        }
        this._pipeline.render(cameraList);
        this._device.present();
    }
    if (this._batcher) this._batcher.reset();
}

下面这个函数负责遍历所有ui渲染节点

  1. this._screens 即Canvas组件,循环遍历所有Canvas
  2. this.walk(screen.node); 这行代码以Canvas为起点,内部递归遍历所有子节点,准备必要的渲染数据
// batcher-2d.ts
public update () {
    if (JSB) {
        return;
    }

    const screens = this._screens;
    let offset = 0;
    for (let i = 0; i < screens.length; ++i) {
        const screen = screens[i];
        const scene = screen._getRenderScene();
        if (!screen.enabledInHierarchy || !scene) {
            continue;
        }
        // Reset state and walk
        this._opacityDirty = 0;
        this._pOpacity = 1;
        this.walk(screen.node);
        this.autoMergeBatches(this._currComponent!);
        this.resetRenderStates();
        let batchPriority = 0;
        if (this._batches.length > offset) {
            for (; offset < this._batches.length; ++offset) {
                const batch = this._batches.array[offset];
                if (batch.model) {
                    const subModels = batch.model.subModels;
                    for (let j = 0; j < subModels.length; j++) {
                        subModels[j].priority = batchPriority++;
                    }
                } else {
                    batch.descriptorSet = this._descriptorSetCache.getDescriptorSet(batch);
                }
                scene.addBatch(batch);
            }
        }
    }
}

再看walk函数,此函数内部负责计算ui父子节点级联的opacity,以及在fillBuffers中填充顶点数据。

  1. fillBuffers内调用的是各个ui渲染组件的_render函数,以label和sprite组件为例,最终调用的是batcher-2d.ts中的commitComp函数
  2. commitComp函数内部负责合批的判断,并进行渲染数据的提交。通常所说的draw call数量就是由此处渲染数据batches数量决定的。
  3. 需要注意的是3.x版本开始合批判断变得非常严格,所有运行时材质实例的使用都会导致不能合批,如果有自定义渲染的需求,需要自行修改此处判断。(注:Native合批判断在c++层)
// batcher-2d.ts
public walk (node: Node, level = 0) {
    if (!node.activeInHierarchy) {
        return;
    }
    const children = node.children;
    const uiProps = node._uiProps;
    const render = uiProps.uiComp as UIRenderer;
    // Save opacity
    const parentOpacity = this._pOpacity;
    let opacity = parentOpacity;
    // TODO Always cascade ui property's local opacity before remove it
    const selfOpacity = render && render.color ? render.color.a / 255 : 1;
    this._pOpacity = opacity *= selfOpacity * uiProps.localOpacity;
    // TODO Set opacity to ui property's opacity before remove it
    // @ts-expect-error temporary force set, will be removed with ui property's opacity
    uiProps._opacity = opacity;
    if (uiProps.colorDirty) {
        // Cascade color dirty state
        this._opacityDirty++;
    }

    // Render assembler update logic
    if (render && render.enabledInHierarchy) {
        render.fillBuffers(this);// for rendering
    }

    // Update cascaded opacity to vertex buffer
    if (this._opacityDirty && render && !render.useVertexOpacity && render.renderData && render.renderData.vertexCount > 0) {
        // HARD COUPLING
        updateOpacity(render.renderData, opacity);
        const buffer = render.renderData.getMeshBuffer();
        if (buffer) {
            buffer.setDirty();
        }
    }

    if (children.length > 0 && !node._static) {
        for (let i = 0; i < children.length; ++i) {
            const child = children[i];
            this.walk(child, level);
        }
    }

    if (uiProps.colorDirty) {
        // Reduce cascaded color dirty state
        this._opacityDirty--;
        // Reset color dirty
        uiProps.colorDirty = false;
    }

    // Restore opacity
    this._pOpacity = parentOpacity;
    // Post render assembler update logic
    // ATTENTION: Will also reset colorDirty inside postUpdateAssembler
    if (render && render.enabledInHierarchy) {
        render.postUpdateAssembler(this);
    }
    level += 1;
}

下面是commitComp函数

  1. if (this._currHash !== dataHash || dataHash === 0 || this._currMaterial !== mat || this._currDepthStencilStateStage !== depthStencilStateStage) 这个if判断即合批判断,直接以this._currMaterial !== mat进行判断意味着所有使用材质实例的地方都将无法合批
  2. this.autoMergeBatches(this._currComponent!); 即一次合批判断的终止,并进行一次渲染数据的提交
  3. assembler.fillBuffers(comp, this); 最后一行代码会调用渲染组件的assembler,负责进行顶点缓冲数据(VBO)、索引缓冲数据(IBO)的填充。
// batcher-2d.ts
public commitComp (comp: UIRenderer, renderData: BaseRenderData|null, frame: TextureBase|SpriteFrame|null, assembler, transform: Node|null) {
    let dataHash = 0;
    let mat;
    let bufferID = -1;
    if (renderData && renderData.chunk) {
        if (!renderData.isValid()) return;
        dataHash = renderData.dataHash;
        mat = renderData.material;
        bufferID = renderData.chunk.bufferId;
    }

    comp.stencilStage = StencilManager.sharedManager!.stage;
    const depthStencilStateStage = comp.stencilStage;
    if (this._currHash !== dataHash || dataHash === 0 || this._currMaterial !== mat
        || this._currDepthStencilStateStage !== depthStencilStateStage) {
        // Merge all previous data to a render batch, and update buffer for next render data
        this.autoMergeBatches(this._currComponent!);
        if (renderData && !renderData._isMeshBuffer) {
            this.updateBuffer(renderData.vertexFormat, bufferID);
        }

        this._currRenderData = renderData;
        this._currHash = renderData ? renderData.dataHash : 0;
        this._currComponent = comp;
        this._currTransform = transform;
        this._currMaterial = comp.getRenderMaterial(0)!;
        this._currDepthStencilStateStage = depthStencilStateStage;
        this._currLayer = comp.node.layer;

        if (frame) {
            this._currTexture = frame.getGFXTexture();
            this._currSampler = frame.getGFXSampler();
            this._currTextureHash = frame.getHash();
            this._currSamplerHash = this._currSampler.hash;
        } else {
            this._currTexture = null;
            this._currSampler = null;
            this._currTextureHash = 0;
            this._currSamplerHash = 0;
        }
    }
    assembler.fillBuffers(comp, this);
}

Native渲染流程

如下所示,在Native上使用C++层实现的frameMove覆盖了JS层的frameMove

// root.jsb.ts
export const Root = jsb.Root;
const rootProto: any = Root.prototype;
const oldFrameMove = rootProto.frameMove;
rootProto.frameMove = function (deltaTime: number) {
    oldFrameMove.call(this, deltaTime, legacyCC.director.getTotalFrames());
};

下面让我们开始看C++层的源码,首先是通过JSB绑定C++层的frameMove接口供JS调用

// jsb_scene_auto.cpp
static bool js_scene_Root_frameMove(se::State& s) // NOLINT(readability-identifier-naming)
{
    auto* cobj = SE_THIS_OBJECT<cc::Root>(s);
    // SE_PRECONDITION2(cobj, false, "Invalid Native Object");
    if (nullptr == cobj) return true;
    const auto& args = s.args();
    size_t argc = args.size();
    CC_UNUSED bool ok = true;
    if (argc == 2) {
        HolderType<float, false> arg0 = {};
        HolderType<int32_t, false> arg1 = {};
        ok &= sevalue_to_native(args[0], &arg0, s.thisObject());
        ok &= sevalue_to_native(args[1], &arg1, s.thisObject());
        SE_PRECONDITION2(ok, false, "Error processing arguments");
        cobj->frameMove(arg0.value(), arg1.value());
        return true;
    }
    SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 2);
    return false;
}
SE_BIND_FUNC(js_scene_Root_frameMove)

C++的frameMove函数的结构其实和JS层看上去差不多,重点还是关注这行代码_batcher->update();

// Root.cpp
void Root::frameMove(float deltaTime, int32_t totalFrames) {
    CCObject::deferredDestroy();
    _frameTime = deltaTime;
    ++_frameCount;
    _cumulativeTime += deltaTime;
    _fpsTime += deltaTime;

    if (_fpsTime > 1.0F) {
        _fps = _frameCount;
        _frameCount = 0;
        _fpsTime = 0.0;
    }

    for (const auto &scene : _scenes) {
        scene->removeBatches();
    }

    if (_batcher != nullptr) {
        _batcher->update();
    }
    _cameraList.clear();
    for (const auto &window : _windows) {
        window->extractRenderCameras(_cameraList);
    }

    if (_pipelineRuntime != nullptr && !_cameraList.empty()) {
        _swapchains.clear();
        _swapchains.emplace_back(_swapchain);
        _device->acquire(_swapchains);
        // NOTE: c++ doesn't have a Director, so totalFrames need to be set from JS
        uint32_t stamp = totalFrames;

        if (_batcher != nullptr) {
            _batcher->uploadBuffers();
        }

        for (const auto &scene : _scenes) {
            scene->update(stamp);
        }

        CC_PROFILER_UPDATE;
        _eventProcessor->emit(EventTypesToJS::DIRECTOR_BEFORE_COMMIT, this);
        std::stable_sort(_cameraList.begin(), _cameraList.end(), [](const auto *a, const auto *b) {
            return a->getPriority() < b->getPriority();
        });

#if !defined(CC_SERVER_MODE)
    #if CC_USE_GEOMETRY_RENDERER
        for (auto *camera : _cameraList) {
            if (camera->getGeometryRenderer()) {
                camera->getGeometryRenderer()->update();
            }
        }
    #endif
        _pipelineRuntime->render(_cameraList);
#endif

        _device->present();
    }

    if (_batcher != nullptr) {
        _batcher->reset();
    }
}

可以看到C++层同样实现了一个walk函数,它的作用和JS层的walk函数类似,也是负责计算ui父子节点级联的opacity,以及填充部分顶点数据。为什么是部分顶点数据?因为uv数据在JS层填充了

// Batcher2d.cpp
void Batcher2d::update() {
    fillBuffersAndMergeBatches();
    resetRenderStates();
    for (const auto& scene : Root::getInstance()->getScenes()) {
        for (auto* batch : _batches) {
            scene->addBatch(batch);
        }
    }
}

void Batcher2d::fillBuffersAndMergeBatches() {
    for (auto* rootNode : _rootNodeArr) {
        walk(rootNode, 1);
        generateBatch(_currEntity, _currDrawInfo);
    }
}

void Batcher2d::walk(Node* node, float parentOpacity) { // NOLINT(misc-no-recursion)
    if (!node->isActiveInHierarchy()) {
        return;
    }

    bool breakWalk = false;
    auto* entity = static_cast<RenderEntity*>(node->getUserData());
    if (entity) {
        if (entity->getColorDirty()) {
            float localOpacity = entity->getLocalOpacity();
            float localColorAlpha = entity->getColorAlpha();
            entity->setOpacity(parentOpacity * localOpacity * localColorAlpha);
            entity->setColorDirty(false);
            entity->setVBColorDirty(true);
        }

        if (entity->isEnabled()) {
            uint32_t size = entity->getRenderDrawInfosSize();
            for (uint32_t i = 0; i < size; i++) {
                auto* drawInfo = entity->getRenderDrawInfoAt(i);
                handleDrawInfo(entity, drawInfo, node);
            }
            entity->setVBColorDirty(false);
        }

        if (entity->getRenderEntityType() == RenderEntityType::CROSSED) {
            breakWalk = true;
        }
    }

    if (!breakWalk) {
        const auto& children = node->getChildren();
        float thisOpacity = entity ? entity->getOpacity() : parentOpacity;
        for (const auto& child : children) {
            // we should find parent opacity recursively upwards if it doesn't have an entity.
            walk(child, thisOpacity);
        }
    }

    // post assembler
    if (_stencilManager->getMaskStackSize() > 0 && entity && entity->isEnabled()) {
        handlePostRender(entity);
    }
}
  1. 同样以label和sprite为例,会在handleComponentDraw函数中进行合批判断与渲染数据的提交
  2. generateBatch(_currEntity, _currDrawInfo) 即一次合批判断的终止,并进行一次渲染数据的提交
  3. fillVertexBuffers函数负责填充position,fillColors函数负责填充color
  4. 通过下面这个函数可以发现,C++层只会负责position和color数据的填充,而uv数据已经在JS层填充了
// Batcher2d.cpp
CC_FORCE_INLINE void Batcher2d::handleComponentDraw(RenderEntity* entity, RenderDrawInfo* drawInfo, Node* node) {
    ccstd::hash_t dataHash = drawInfo->getDataHash();
    if (drawInfo->getIsMeshBuffer()) {
        dataHash = 0;
    }

    entity->setEnumStencilStage(_stencilManager->getStencilStage());
    auto tempStage = static_cast<StencilStage>(entity->getStencilStage());

    if (_currHash != dataHash || dataHash == 0 || _currMaterial != drawInfo->getMaterial() || _currStencilStage != tempStage) {
        // Generate a batch if not batching
        generateBatch(_currEntity, _currDrawInfo);
        bool isSubMask = entity->getIsSubMask();

        if (isSubMask) {
            // Mask subComp
            _stencilManager->enterLevel(entity);
        }

        if (!drawInfo->getIsMeshBuffer()) {
            UIMeshBuffer* buffer = drawInfo->getMeshBuffer();
            if (_currMeshBuffer != buffer) {
                _currMeshBuffer = buffer;
                _indexStart = _currMeshBuffer->getIndexOffset();
            }
        }

        _currHash = dataHash;
        _currMaterial = drawInfo->getMaterial();
        _currStencilStage = tempStage;
        _currLayer = entity->getNode()->getLayer();
        _currEntity = entity;
        _currDrawInfo = drawInfo;
        _currTexture = drawInfo->getTexture();
        _currSampler = drawInfo->getSampler();

        if (_currSampler == nullptr) {
            _currSamplerHash = 0;
        } else {
            _currSamplerHash = _currSampler->getHash();
        }
    }

    if (!drawInfo->getIsMeshBuffer()) {
        if (node->getChangedFlags() || drawInfo->getVertDirty()) {
            fillVertexBuffers(entity, drawInfo);
            drawInfo->setVertDirty(false);
        }
        if (entity->getVBColorDirty()) {
            fillColors(entity, drawInfo);
        }
        fillIndexBuffers(drawInfo);
    }
}

以上就是Native的2d渲染流程的重点,除此之外,如果有在Native中自定义顶点数据的需求的话,还需要关注一下C++层的UIMeshBuffer,Native的顶点数据格式在此定义。

  1. 可以看到C++层ui默认的顶点数据格式如下所示,且目前暂未看到有提供接口给JS层去自定义
  2. 渲染数据初始化时会调用initialize,可以通过修改此函数来实现自定义的顶点数据格式
// UIMeshBuffer.h
ccstd::vector<gfx::Attribute> _attributes{
    gfx::Attribute{gfx::ATTR_NAME_POSITION, gfx::Format::RGB32F},
    gfx::Attribute{gfx::ATTR_NAME_TEX_COORD, gfx::Format::RG32F},
    gfx::Attribute{gfx::ATTR_NAME_COLOR, gfx::Format::RGBA32F},
};

// UIMeshBuffer.cpp
void UIMeshBuffer::initialize(gfx::Device*  /*device*/, ccstd::vector<gfx::Attribute*>&& attrs, uint32_t  /*vFloatCount*/, uint32_t  /*iCount*/) {
    if (attrs.size() == 4) {
        _attributes.push_back(gfx::Attribute{gfx::ATTR_NAME_COLOR2, gfx::Format::RGBA32F});
    }
}
26赞

人才呀,先收藏,再慢慢看了。

写得不错,正好最近在看引擎源码。基本上和3.7的引擎源码差别不大。

膜拜大佬!

想写一个组件,需要修改一点渲染相关源码,本来没头绪,这篇解读帮助很大!