1. 背景 Cocos2d-x 3.9 原版 TTF Label 通过 CustomCommand 提交渲染。Renderer 执行 CustomCommand 前会刷新已经积累的批次,因此多个 TTF Label 即使使用相同字体、字号和材质,也无法跨 Label 动态合批。
官方源码参考:
本次改造把 TTF Label 已经生成的字体顶点和索引接入 TrianglesCommand,复用 Renderer 原有的三角形动态合批流程。
1 2 3 原版:Label -> CustomCommand -> TextureAtlas::drawQuads() -> 独立 DrawCall 改造:Label -> TrianglesCommand -> Renderer::drawBatchedTriangles() -> 动态合批
2. 涉及文件 1 2 3 4 5 6 7 8 9 frameworks/cocos2d-x/cocos/2d/CCLabel.cpp frameworks/cocos2d-x/cocos/2d/CCLabel.h frameworks/cocos2d-x/cocos/renderer/CCTextureAtlas.cpp frameworks/cocos2d-x/cocos/renderer/CCTextureAtlas.h frameworks/cocos2d-x/cocos/renderer/CCTrianglesCommand.cpp frameworks/cocos2d-x/cocos/renderer/CCTrianglesCommand.h frameworks/cocos2d-x/cocos/renderer/ccShader_Label.vert frameworks/cocos2d-x/cocos/renderer/ccShader_Label_normal.frag frameworks/cocos2d-x/cocos/renderer/ccShader_Label_df.frag
3. TextureAtlas 导出三角形数据 TTF 的字符 Quad 已保存在 TextureAtlas 中,不需要重新构建一套顶点数据。增加一个接口,将现有 Quad 和索引交给 TrianglesCommand。
CCTextureAtlas.h1 2 3 4 5 6 7 8 #include "renderer/CCTrianglesCommand.h" class CC_DLL TextureAtlas : public Ref{ public : void drawToTriangles (TrianglesCommand::Triangles* triangles) ; };
CCTextureAtlas.cpp1 2 3 4 5 6 7 8 void TextureAtlas::drawToTriangles (TrianglesCommand::Triangles* triangles) { CCASSERT (triangles, "triangles must not be null" ); triangles->verts = reinterpret_cast <V3F_C4B_T2F*>(_quads); triangles->indices = _indices; triangles->vertCount = _totalQuads * 4 ; triangles->indexCount = _totalQuads * 6 ; }
这里没有复制正文顶点。TrianglesCommand 在 Renderer 真正执行前必须始终能够访问 _quads 和 _indices,因此 TextureAtlas 的生命周期不能早于渲染命令。
4. 扩展 TrianglesCommand 的材质判定 4.1 原版限制 Cocos2d-x 3.9 原版 TrianglesCommand 遇到包含用户 uniform 的 GLProgramState 时,会直接禁止合批:
1 2 3 4 if (_glProgramState->getUniformCount () > 0 ){ _materialID = Renderer::MATERIAL_ID_DO_NOT_BATCH; }
Label 的描边、发光等 Shader 仍需要 u_effectColor,所以仅把 CustomCommand 改成 TrianglesCommand 还不够。
4.2 新增接口和字段 CCTrianglesCommand.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <functional> class CC_DLL TrianglesCommand : public RenderCommand{ public : void init (float globalOrder, GLuint textureID, GLProgramState* glProgramState, BlendFunc blendType, const Triangles& triangles, const Mat4& mv, uint32_t flags, const std::function<void (TrianglesCommand*)>& applyMaterialFunc, uint32_t materialValue, bool allowBatchingWithUniforms) ; inline GLProgramState* getGLProgramState () const { return _glProgramState; } protected : uint32_t _materialValue; bool _allowBatchingWithUniforms; std::function<void (TrianglesCommand*)> _applyMaterialFunc; };
三个参数的职责如下:
参数
作用
applyMaterialFunc
命令真正使用材质前写入当前 Label 的 uniform
materialValue
把会影响 Shader 输出的 uniform 值纳入材质标识
allowBatchingWithUniforms
声明调用者已经完整描述 uniform,可以安全参与合批
4.3 初始化 CCTrianglesCommand.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 TrianglesCommand::TrianglesCommand () : _materialID(0 ) , _textureID(0 ) , _materialValue(0 ) , _allowBatchingWithUniforms(false ) , _glProgramState(nullptr ) , _blendType(BlendFunc::DISABLE) { _type = RenderCommand::Type::TRIANGLES_COMMAND; } void TrianglesCommand::init (float globalOrder, GLuint textureID, GLProgramState* glProgramState, BlendFunc blendType, const Triangles& triangles, const Mat4& mv, uint32_t flags) { init (globalOrder, textureID, glProgramState, blendType, triangles, mv, flags, nullptr , 0 , false ); } void TrianglesCommand::init ( float globalOrder, GLuint textureID, GLProgramState* glProgramState, BlendFunc blendType, const Triangles& triangles, const Mat4& mv, uint32_t flags, const std::function<void (TrianglesCommand*)>& applyMaterialFunc, uint32_t materialValue, bool allowBatchingWithUniforms) { CCASSERT (glProgramState, "Invalid GLProgramState" ); CCASSERT (glProgramState->getVertexAttribsFlags () == 0 , "No custom attributes are supported in QuadCommand" ); RenderCommand::init (globalOrder, mv, flags); _triangles = triangles; if (_triangles.indexCount % 3 != 0 ) { ssize_t count = _triangles.indexCount; _triangles.indexCount = count / 3 * 3 ; CCLOGERROR ("Resize indexCount from %zd to %zd, size must be multiple times of 3" , count, _triangles.indexCount); } _mv = mv; _applyMaterialFunc = applyMaterialFunc; if (_textureID != textureID || _blendType.src != blendType.src || _blendType.dst != blendType.dst || _glProgramState != glProgramState || _materialValue != materialValue || _allowBatchingWithUniforms != allowBatchingWithUniforms) { _textureID = textureID; _blendType = blendType; _glProgramState = glProgramState; _materialValue = materialValue; _allowBatchingWithUniforms = allowBatchingWithUniforms; generateMaterialID (); } }
4.4 生成 Material ID 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 void TrianglesCommand::generateMaterialID () { if (_glProgramState->getUniformCount () > 0 && !_allowBatchingWithUniforms) { _materialID = Renderer::MATERIAL_ID_DO_NOT_BATCH; } else if (_allowBatchingWithUniforms) { struct { GLProgramState* glProgramState; GLuint textureID; GLenum blendSrc; GLenum blendDst; uint32_t materialValue; } hashMe = {}; hashMe.glProgramState = _glProgramState; hashMe.textureID = _textureID; hashMe.blendSrc = _blendType.src; hashMe.blendDst = _blendType.dst; hashMe.materialValue = _materialValue; _materialID = XXH32 (&hashMe, sizeof (hashMe), 0 ); } else { int glProgram = static_cast <int >(_glProgramState->getGLProgram ()->getProgram ()); int intArray[4 ] = { glProgram, static_cast <int >(_textureID), static_cast <int >(_blendType.src), static_cast <int >(_blendType.dst) }; _materialID = XXH32 (intArray, sizeof (intArray), 0 ); } }
必须把 GLProgramState 本身放入哈希,不能只使用 GL Program ID。不同 GLProgramState 可能拥有不同的 uniform 状态和回调语义。
1 2 3 4 5 6 7 8 9 10 11 12 void TrianglesCommand::useMaterial () const { GL::bindTexture2D (_textureID); GL::blendFunc (_blendType.src, _blendType.dst); if (_applyMaterialFunc) { _applyMaterialFunc(const_cast <TrianglesCommand*>(this )); } _glProgramState->apply (_mv); }
回调必须在 GLProgramState::apply() 前执行,否则本次绘制仍可能使用上一条命令遗留的 uniform。
5. 完整 Shader 普通文字颜色和透明度迁移到顶点颜色后,不同颜色的 Label 不再需要依赖 u_textColor,因此仍然可以合批。
5.1 ccShader_Label.vert 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const char* ccLabel_vert = STRINGIFY(attribute vec4 a_position;attribute vec2 a_texCoord;attribute vec4 a_color;\n#ifdef GL_ES\n varying lowp vec4 v_fragmentColor;varying mediump vec2 v_texCoord;\n#else\n varying vec4 v_fragmentColor;varying vec2 v_texCoord;\n#endif\n void main(){ gl_Position = CC_PMatrix * a_position; v_fragmentColor = a_color; v_texCoord = a_texCoord; } );
这里必须使用 CC_PMatrix。Renderer::fillVerticesAndIndices() 已在 CPU 侧把每条 TrianglesCommand 的顶点乘以 ModelView 矩阵;如果 Shader 再使用 CC_MVPMatrix,ModelView 会被重复应用。
5.2 ccShader_Label_normal.frag 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const char* ccLabelNormal_frag = STRINGIFY(\n#ifdef GL_ES\n precision lowp float ;\n#endif\n varying vec4 v_fragmentColor;varying vec2 v_texCoord;void main(){ gl_FragColor = v_fragmentColor * vec4 ( 1.0 , 1.0 , 1.0 , texture2D (CC_Texture0, v_texCoord).a ); } );
颜色和 Label 透明度来自 v_fragmentColor,字体纹理只提供字符 Alpha。
5.3 ccShader_Label_df.frag 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const char* ccLabelDistanceFieldNormal_frag = STRINGIFY(\n#ifdef GL_ES\n precision lowp float ;\n#endif\n varying vec4 v_fragmentColor;varying vec2 v_texCoord;void main(){ vec4 color = texture2D (CC_Texture0, v_texCoord); float dist = color.a; float width = 0.04 ; float alpha = smoothstep (0.5 - width, 0.5 + width, dist); gl_FragColor = v_fragmentColor * vec4 (1.0 , 1.0 , 1.0 , alpha); } );
DistanceField 普通文字同样使用顶点颜色。描边和发光 Shader 仍可保留 u_effectColor,但效果颜色必须进入 materialValue。
6. Label 保存和释放渲染命令 CCLabel.h1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include "renderer/CCTrianglesCommand.h" class CC_DLL Label : public Node, public LabelProtocol{ protected : void drawTTF (Renderer* renderer, const Mat4& transform, uint32_t flags) ; uint32_t getTTFMaterialValue ( const Color4F* textColor, const Color4F* effectColor) const ; std::vector<TrianglesCommand*> _ttfCommands; std::vector<TrianglesCommand*> _ttfShadowCommands; std::vector<std::vector<V3F_C4B_T2F>> _ttfShadowVertices; };
每个字体 atlas page 对应一条正文命令;开启阴影后,每页还需要一条阴影命令。
CCLabel.cpp 析构释放1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Label::~Label () { delete [] _horizontalKernings; for (auto command : _ttfCommands) { delete command; } for (auto command : _ttfShadowCommands) { delete command; } }
7. 计算 TTF 材质值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 uint32_t Label::getTTFMaterialValue ( const Color4F* textColor, const Color4F* effectColor) const { float colors[8 ] = { 0.0f , 0.0f , 0.0f , 0.0f , 0.0f , 0.0f , 0.0f , 0.0f }; if (textColor) { colors[0 ] = textColor->r; colors[1 ] = textColor->g; colors[2 ] = textColor->b; colors[3 ] = textColor->a; } if (effectColor) { colors[4 ] = effectColor->r; colors[5 ] = effectColor->g; colors[6 ] = effectColor->b; colors[7 ] = effectColor->a; } return XXH32 (colors, sizeof (colors), 0 ); }
普通 Shader 的文字颜色已经进入顶点,所以 textColor 可以传 nullptr。仍通过 uniform 传递的颜色必须参与哈希。
8. Label::drawTTF() 关键实现 下面是正文和阴影提交的完整核心逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 void Label::drawTTF (Renderer* renderer, const Mat4& transform, uint32_t flags) { for (auto && it : _letters) { it.second->updateTransform (); } size_t batchCount = static_cast <size_t >(_batchNodes.size ()); while (_ttfCommands.size () < batchCount) { auto command = new (std::nothrow) TrianglesCommand (); if (!command) { CCLOGERROR ("Failed to allocate TTF triangles command" ); return ; } _ttfCommands.push_back (command); } while (_ttfShadowCommands.size () < batchCount) { auto command = new (std::nothrow) TrianglesCommand (); if (!command) { CCLOGERROR ("Failed to allocate TTF shadow triangles command" ); return ; } _ttfShadowCommands.push_back (command); } const bool hasEffect = _currLabelEffect == LabelEffect::OUTLINE || _currLabelEffect == LabelEffect::GLOW; const Color4F* effectColor = hasEffect ? &_effectColorF : nullptr ; auto programState = getGLProgramState (); auto textColorLocation = static_cast <GLint>(_uniformTextColor); const bool textColorInUniform = textColorLocation >= 0 ; GLint effectColorLocation = hasEffect ? static_cast <GLint>(_uniformEffectColor) : -1 ; if (_shadowEnabled) { if (!textColorInUniform && _ttfShadowVertices.size () < batchCount) { _ttfShadowVertices.resize (batchCount); } Color4F shadowColor = _shadowColor4F; auto shadowMaterialValue = getTTFMaterialValue ( textColorInUniform ? &shadowColor : nullptr , hasEffect ? &shadowColor : nullptr ); auto getShadowVertexColor = [this ](const Color3B& displayedColor, GLubyte displayedOpacity) { Color4B color ( displayedColor.r, displayedColor.g, displayedColor.b, displayedOpacity); if (_isOpacityModifyRGB) { color.r *= displayedOpacity / 255.0f ; color.g *= displayedOpacity / 255.0f ; color.b *= displayedOpacity / 255.0f ; } color.r *= _shadowColor3B.r / 255.0f ; color.g *= _shadowColor3B.g / 255.0f ; color.b *= _shadowColor3B.b / 255.0f ; color.a *= _shadowOpacity / 255.0f ; return color; }; Color4B shadowVertexColor = getShadowVertexColor ( _displayedColor, _displayedOpacity); for (size_t index = 0 ; index < batchCount; ++index) { auto textureAtlas = _batchNodes.at (index)->getTextureAtlas (); TrianglesCommand::Triangles triangles; textureAtlas->drawToTriangles (&triangles); if (triangles.indexCount == 0 ) { continue ; } if (!textColorInUniform) { auto & shadowVertices = _ttfShadowVertices[index]; shadowVertices.assign ( triangles.verts, triangles.verts + triangles.vertCount); for (auto & vertex : shadowVertices) { vertex.colors = shadowVertexColor; } for (auto && letterIt : _letters) { auto letter = letterIt.second; if (letter->getTextureAtlas () != textureAtlas) { continue ; } auto atlasIndex = letter->getAtlasIndex (); if (atlasIndex < 0 ) { continue ; } auto vertexOffset = static_cast <size_t >(atlasIndex) * 4 ; if (vertexOffset + 4 > shadowVertices.size ()) { continue ; } auto letterShadowColor = getShadowVertexColor ( letter->getDisplayedColor (), letter->getDisplayedOpacity ()); for (size_t vertexIndex = vertexOffset; vertexIndex < vertexOffset + 4 ; ++vertexIndex) { shadowVertices[vertexIndex].colors = letterShadowColor; } } triangles.verts = shadowVertices.data (); } auto command = _ttfShadowCommands[index]; command->init ( _globalZOrder, textureAtlas->getTexture ()->getName (), programState, _blendFunc, triangles, _shadowTransform, flags, [textColorLocation, effectColorLocation, shadowColor, textColorInUniform, hasEffect](TrianglesCommand* materialCommand) { auto state = materialCommand->getGLProgramState (); if (textColorInUniform) { state->setUniformVec4 ( textColorLocation, Vec4 (shadowColor.r, shadowColor.g, shadowColor.b, shadowColor.a)); } if (hasEffect) { state->setUniformVec4 ( effectColorLocation, Vec4 (shadowColor.r, shadowColor.g, shadowColor.b, shadowColor.a)); } }, shadowMaterialValue, true ); renderer->addCommand (command); } } Color4F textColor = _textColorF; Color4F currentEffectColor = _effectColorF; auto materialValue = getTTFMaterialValue ( textColorInUniform ? &textColor : nullptr , effectColor); for (size_t index = 0 ; index < batchCount; ++index) { auto textureAtlas = _batchNodes.at (index)->getTextureAtlas (); TrianglesCommand::Triangles triangles; textureAtlas->drawToTriangles (&triangles); if (triangles.indexCount == 0 ) { continue ; } auto command = _ttfCommands[index]; command->init ( _globalZOrder, textureAtlas->getTexture ()->getName (), programState, _blendFunc, triangles, transform, flags, [textColorLocation, effectColorLocation, textColor, currentEffectColor, textColorInUniform, hasEffect](TrianglesCommand* materialCommand) { auto state = materialCommand->getGLProgramState (); if (textColorInUniform) { state->setUniformVec4 ( textColorLocation, Vec4 (textColor.r, textColor.g, textColor.b, textColor.a)); } if (hasEffect) { state->setUniformVec4 ( effectColorLocation, Vec4 (currentEffectColor.r, currentEffectColor.g, currentEffectColor.b, currentEffectColor.a)); } }, materialValue, true ); renderer->addCommand (command); } }
在 Label::draw() 中,仅让 TTF 走新的提交路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (_currentLabelType == LabelType::TTF){ drawTTF (renderer, transform, flags); } else if (!_shadowEnabled && (_currentLabelType == LabelType::BMFONT || _currentLabelType == LabelType::CHARMAP)) { } else { }
9. 修复逐字动画颜色 当普通文字颜色进入顶点后,LabelLetter 更新单个字符颜色时不能覆盖 Label 原始文字颜色。
9.1 保存 Label 文字颜色 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class LabelLetter : public Sprite{ public : LabelLetter () : _labelTextColor(Color4B::WHITE) { _textureAtlas = nullptr ; } void setLabelTextColor (const Color4B& color) { _labelTextColor = color; updateColor (); } virtual void updateColor () override { if (_textureAtlas == nullptr ) { return ; } Color4B color4 ( _displayedColor.r, _displayedColor.g, _displayedColor.b, _displayedOpacity) ; color4.r *= _labelTextColor.r / 255.0f ; color4.g *= _labelTextColor.g / 255.0f ; color4.b *= _labelTextColor.b / 255.0f ; color4.a *= _labelTextColor.a / 255.0f ; if (_opacityModifyRGB) { color4.r *= _displayedOpacity / 255.0f ; color4.g *= _displayedOpacity / 255.0f ; color4.b *= _displayedOpacity / 255.0f ; } _quad.bl.colors = color4; _quad.br.colors = color4; _quad.tl.colors = color4; _quad.tr.colors = color4; _textureAtlas->updateQuad (&_quad, _atlasIndex); } private : Color4B _labelTextColor; };
9.2 创建字符时同步颜色 1 2 3 4 5 6 7 8 9 10 if (_currentLabelType == LabelType::TTF){ letter->updateDisplayedColor (_displayedColor); auto labelLetter = static_cast <LabelLetter*>(letter); labelLetter->setLabelTextColor ( _currLabelEffect == LabelEffect::NORMAL ? _textColor : Color4B::WHITE); }
描边和发光模式的正文颜色仍可能由对应 Shader uniform 处理,因此这里使用白色顶点基色,避免重复乘色。
10. 修复逐字动画阴影提前显示 不能只给整份阴影顶点写入 Label 的整体透明度。已经通过 getLetter() 控制的字符必须使用自己的 displayedColor 和 displayedOpacity。
关键映射关系是:
1 auto vertexOffset = static_cast <size_t >(letter->getAtlasIndex ()) * 4 ;
每个字符对应四个顶点:
1 2 3 4 5 6 7 8 9 10 auto letterShadowColor = getShadowVertexColor ( letter->getDisplayedColor (), letter->getDisplayedOpacity ()); for (size_t vertexIndex = vertexOffset; vertexIndex < vertexOffset + 4 ; ++vertexIndex) { shadowVertices[vertexIndex].colors = letterShadowColor; }
因此,逐字动画中透明度为 0 的字符,其四个阴影顶点也为透明,不会提前显示完整阴影。
11. 哪些配置能够合批 Renderer 只会合并相邻且 Material ID 相同的 TrianglesCommand。
可以不同 以下配置主要改变顶点和布局,通常不阻止合批:
文字内容,但字符必须落在同一字体纹理页。
位置、旋转和缩放。
普通文字颜色和透明度。
对齐、换行和 Label 尺寸。
setLineSpacing()。
setAdditionalKerning()。
必须兼容或保持一致
字体文件。
FontSize。
实际使用的字体纹理页。
有效 DistanceField 状态。
OutlineSize。
Shader 和 GLProgramState。
BlendFunc。
所有不能存入顶点、但会影响 Shader 输出的 uniform。
不同 FontSize 会生成不同 FontAtlas,通常对应不同纹理,因此不能合批。若视觉允许,可以使用相同 FontSize 配合 setScale(),但放大过多会影响清晰度。
12. 阴影合批限制 阴影是独立的渲染命令。实际命令顺序可能是:
1 2 3 4 Label1 Shadow Label1 Text Label2 Shadow Label2 Text
由于正文命令夹在两个阴影命令之间,Renderer 不会跨命令重排透明对象,所以不能保证多个带阴影 Label 最终固定为两个 DrawCall。
13. 验证用例 建议在独立场景中测量 DrawCall 增量,而不是观察复杂场景的绝对值。
用例
预期结果
相同字体和字号,不同文本
同一纹理页内可以合批
相同字体和字号,不同普通颜色
可以合批
不同 FontSize
不合批
不同 setLineSpacing()
可以合批
不同 setAdditionalKerning()
可以合批
不同 OutlineSize
通常不合批
不同描边颜色
取决于材质值;不同 uniform 值必须拆批
中间插入 Sprite 或 CustomCommand
相邻批次被切断
逐字动画还必须检查:
动画期间文字保持原来设置的橙色。
未出现字符的阴影不会提前显示。
描边外观与合批改造前一致。
14. 风险
新增 Shader uniform 时,如果没有加入 materialValue,可能发生错误合批。
TrianglesCommand 保存的是顶点指针,必须保证顶点缓冲在命令执行前有效。
GLProgramState、纹理、BlendFunc 或材质值任意一项不同都会拆批。
FontAtlas 分页会自然产生多条命令。
Renderer 顶点缓冲或索引缓冲达到容量上限时仍会主动 flush。
阴影顶点使用独立副本,避免覆盖正文顶点,但会增加少量 CPU 和内存开销。
本文展示的是 Cocos2d-x 3.9 的实现方式,移植到其他版本时需要重新核对 Renderer 的矩阵变换和合批条件。
15. 总结 TTF Label 合批的关键不在字体栅格化,而在渲染命令和材质状态:
用 TrianglesCommand 替换 TTF 的 CustomCommand。
直接复用 TextureAtlas 的字符顶点和索引。
把普通文字颜色迁移到顶点颜色。
把剩余 uniform 完整纳入 Material ID。
在绘制前恢复当前命令的 uniform。
单独处理逐字动画的字符颜色和阴影透明度。
满足相同纹理、Shader、BlendFunc、材质值和命令相邻等条件后,多个 TTF Label 就可以进入 Cocos2d-x 原有的动态合批流程。
16. 相关阅读