v3.6.0出来后,官方宣传2D原生性能大幅提升,原因是在Native上将很多代码放到C++层去实现了,正好最近我自己看了一遍2D渲染相关的源码,梳理了流程,在此分享一下。
PS: 目前关于自定义渲染的内容,在3.6版本中官方还没有放出像以前2.x那样方便的接口供开发者使用,如果有自定义顶点数据、渲染组件的需求可以参考我这个项目(支持Web与Native):【CocosTextMeshPro】一个文本渲染解决方案——支持字体颜色渐变、斜体、下划线、删除线、描边、镂空、阴影、辉光、顶点动画、新的排版模式
阅读须知
- 本文会大量引用引擎源码,每个代码段注释处都会标明源码文件名
- 水平有限,如有理解错误的地方,希望能够指正
Web与Native共用部分
主循环
-
tick
函数即引擎的主循环,可以看到各个系统的更新都在此处 -
uiRendererManager.updateAllDirtyRenderers();
是所有ui渲染组件顶点数据组装的入口 -
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类型为例
-
this.updateUVs(sprite)
直接更新了uv数据,填充进顶点缓冲(VBO)中 -
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);
-
this._renderDrawInfo
上的数据会绑定到C++层 -
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中的代码
- 准备渲染数据的时候会调用
initRender2dBuffer
,作用是让C++层持有JS层this._render2dBuffer
这个对象的指针 -
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渲染流程
-
frameMove
函数中首先计算了fps -
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渲染节点
-
this._screens
即Canvas组件,循环遍历所有Canvas -
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
中填充顶点数据。
-
fillBuffers
内调用的是各个ui渲染组件的_render
函数,以label和sprite组件为例,最终调用的是batcher-2d.ts
中的commitComp
函数 - 在
commitComp
函数内部负责合批的判断,并进行渲染数据的提交。通常所说的draw call数量就是由此处渲染数据batches数量决定的。 - 需要注意的是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
函数
-
if (this._currHash !== dataHash || dataHash === 0 || this._currMaterial !== mat || this._currDepthStencilStateStage !== depthStencilStateStage)
这个if判断即合批判断,直接以this._currMaterial !== mat
进行判断意味着所有使用材质实例的地方都将无法合批 -
this.autoMergeBatches(this._currComponent!);
即一次合批判断的终止,并进行一次渲染数据的提交 -
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);
}
}
- 同样以label和sprite为例,会在
handleComponentDraw
函数中进行合批判断与渲染数据的提交 -
generateBatch(_currEntity, _currDrawInfo)
即一次合批判断的终止,并进行一次渲染数据的提交 -
fillVertexBuffers
函数负责填充position,fillColors
函数负责填充color - 通过下面这个函数可以发现,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的顶点数据格式在此定义。
- 可以看到C++层ui默认的顶点数据格式如下所示,且目前暂未看到有提供接口给JS层去自定义
- 渲染数据初始化时会调用
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});
}
}