cocos2dx如何让label支持合批

1,342 阅读8分钟

label.vertex

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
void main()
{
    gl_Position = CC_MVPMatrix * a_position;
    v_fragmentColor = a_color;
    v_texCoord = a_texCoord;
}

LabelEffect::NORMAL

正常:距离场_useDistanceField: SHADER_NAME_LABEL_DISTANCEFIELD_NORMAL

正常:_useA8Shader: SHADER_NAME_LABEL_NORMAL

setTTFConfigInternal会触发这里逻辑

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform vec4 u_textColor;
void main()
{
    int textureAlpha = texture2D(CC_Texture0, v_texCoord).a;
    gl_FragColor =  v_fragmentColor * vec4(
        u_textColor.rgb,// RGB from uniform
        u_textColor.a * textureAlpha // A from texture & uniform
    );
}

正常:有阴影 _shadowEnabled: SHADER_NAME_POSITION_TEXTURE_COLOR

  • label.vertex
  • frament
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

void main()
{
    gl_FragColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
}

其他情况 SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP

void main()
{
    gl_Position = CC_PMatrix * a_position;
    v_fragmentColor = a_color;
    v_texCoord = a_texCoord;
}
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

void main()
{
    gl_FragColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
}

LabelEffect::OUTLINE

SHADER_NAME_LABEL_OUTLINE

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

uniform vec4 u_effectColor;
uniform vec4 u_textColor;
uniform int u_effectType;
 
void main()
{
    vec4 sample = texture2D(CC_Texture0, v_texCoord);
    // fontAlpha == 1 means the area of solid text (without edge)
    // fontAlpha == 0 means the area outside text, including outline area
    // fontAlpha == (0, 1) means the edge of text
    float fontAlpha = sample.a;

    // outlineAlpha == 1 means the area of 'solid text' and 'solid outline'
    // outlineAlpha == 0 means the transparent area outside text and outline
    // outlineAlpha == (0, 1) means the edge of outline
    float outlineAlpha = sample.r;

    if (u_effectType == 0) // draw text
    {
        gl_FragColor = v_fragmentColor * vec4(u_textColor.rgb, u_textColor.a * fontAlpha);
    }
    else if (u_effectType == 1) // draw outline
    {
        // multipy (1.0 - fontAlpha) to make the inner edge of outline smoother and make the text itself transparent.
        gl_FragColor = v_fragmentColor * vec4(u_effectColor.rgb, u_effectColor.a * outlineAlpha * (1.0 - fontAlpha));
    }
    else // draw shadow
    {
        gl_FragColor = v_fragmentColor * vec4(u_effectColor.rgb, u_effectColor.a * outlineAlpha);
    }
}

暂时不考虑:LabelEffect::GLOW

_useDistanceField SHADER_NAME_LABEL_DISTANCEFIELD_GLOW

这部分也需要改动,就像unity那样,有一个大的字符纹理图集,里面会填充所有的特征的字符纹理,加粗,阴影,描边也不例外,本质上都是字符纹理

  • 距离场:????

  • 加粗:???? 是glyph会生成加粗的纹理

  • 阴影是2个纹理偏移

  • 描边是用glyph.FT_Stroker_Set() 生成描边纹理后、贴在本体的后边

FontFreeType关联FontAtlas的几个属性:

_fontAscender:

在prepareLetterDefinitions函数中,只有ttf在使用这个属性

fontAscender是一个字体属性,指定字体中字符的上升高度。当我们在使用字体来渲染文本时,这个值告诉我们应该将字符的基线放在哪里,以及字符的顶部如何与其他字符对齐。具体而言,它通常用于计算字符的布局和位置,以确保文本在页面上正确地呈现。

  • _lineHeight

outlineSize

会影响pixelFormat

getEncoding

文本转码

保证在同一个纹理里面

  • FontAtlas现状:cocos现在是将相同特征的字符纹理统一放到了一张纹理里面,每个FontTexture都是某个特征的字符纹理图集,它有很多个slot,当该特征的字符把第一个纹理填满时,就会再开辟第二个纹理

pixel format的选择

描边和非描边纹理的format是不同的,描边的是AI88,不描边是A8

原来的时候是用了2个textureAtlas分开存储,相当于归了一个类别,本质上还是要占用不可缺少的内存空间。

采用AI88

纹理有2个通道:

  • 一个通道:描边纹理
  • 一个通道:非描边纹理

优点:

  • 不用修改shader,可以在一个像素点里面同时取到2个纹理信息,sample.a、sample.r,也就是一个像素点同时取到了描边和非描边的情况

缺点:

  • 没有描边的时候,只使用了一个通道,另一个通道浪费了
  • 布局问题,要把描边的宽度考虑进去,但是因为不知道描边多宽,所以也没办法确认这个字符纹理的宽高,也就导致无法排布,也不可能非描边纹理生成多份和描边纹理一一对应,这样浪费的就多,这个缺点直接导致只能使用A8

采用A8

只有一个通道,shader也要修改:

  • 不同描边宽度的纹理也采用A8存储,并且排布在一个纹理里面,纹理没有冗余
  • 需要传入2个纹理,来确定之前的Luminace、Alpha信息,也能达到同样的效果
    • [?] 如何将描边纹理数据和普通纹理数据对齐

事实上unity就是这么干的

继承包含关系

erDiagram
Label ||--o{ FontAtlas:has
FontAtlas ||--o{ Font:has
Font ||--o{ FontFreeType:inherit
Font ||--o{ FontBMFont:inherit
Font ||--o{ FontCharMap:inherit

FontFreeType/FontBMFnt/FontCharMap都是继承自Font,FontAtlas都会绑定对应的Font,FontAtlas里面掺杂了Font子类的逻辑,需要剥离出来

FontAtlas的职能发生变化

  • 不支持ttf合批:根据ttfConfig,通过对应的FontFreeType渲染出字符纹理,并同步到bind a8/ ai88 texture
  • 支持ttf合批:根据ttfConfig,查找对应的FontFreeType后渲染出字符纹理,并将字符纹理同步到ttf a8 texture

会抽象出FontFreeType的管理类

FontAtlas的改动方向

FontFreeType这个类可以渲染某种特征的字体,所以我们需要有很多这样的类,用来渲染不同特征的字符纹理,而不是FontAtlas对应一个FrontFreeType,label对应一个FontAtlas

  • label
    • FontAtlas
      • FontFreeType1
      • FontFreeType2

同时也需要管理起来,根据label的ttfconfig,查找对应的FrontFreeType来进行渲染该特征的纹理

这样修改后,fontAtlas就需要根据ttfConfig从多个FontFreeType查找

字符信息的记录结构发生变化

需要记录每个特征的字符,出现在哪个纹理里面,以方便渲染的时候知道取大纹理的哪部分,copy到一个渲染纹理的做法也不可取,这样会造成内存浪费。

当渲染某个特征的字符时,我需要一个key知道是否曾经生成过这个字符纹理,因为现在大家都混在了一个纹理里面,并且记录在了一起,所以记录字符纹理信息的key应该是

为了更加直观的看到FontAltasCache里面的字符纹理图集,我需要开发一个字符纹理图集(atlasMap)查看工具,对后续排错非常有帮助,而且对于后续性能优化也能起到非常大的帮助,unity好像就不能查看字符纹理图集,只能借助renderdoc

bmfont和charmap在addLetterDefinition的时候只有charID信息,它没有TTFConfig参数,但是ttf需要这个TTFConfig参数作为key,因为它们目前不在同一个图集,所以

label又同时兼顾到了这三种情况,ttf模式下单靠一个charID是无法区分的,

  • 办法1:是将这个逻辑分发到具体的子类里面
  • 办法2:bmfont/charmap的key和ttf保持一致

很显然采用第二种办法更优,改动更小,因为大家的key对齐方式一样了,底层的处理也就一样了,无非是bmfont/charmap的ttfConfig永远是一个默认值,而ttf的是有效值,对key的生成本质上没有变化

但是这种办法因为key是string,显然有点冗余,但是为了对齐也少不了,后期可以考虑string to hash,字符串比较会消耗比较多的时间

label

label的_fontAtlas里面存储的是相同特征的图集,而label里面的每个文字都是具有相同特征,所以label里面的字符一般都是在一个相同的_fontAtlas里面

lable要实现不同特征的ttf合批,就需要将所有字符都搞到一个texture里面,多纹理也能办到合批,但是有点不现实

_batchNodes

_batchNodes的数量始终会和_fontAtlas的的纹理个数对齐,也就是说一个batchNode管理的就是fontAtlas的某个slot texture

一般来说,label的string都会出现在一个texture里面,这样就只有一个batchNode,当然也就只会触发一次drawQuads

label.string如果是str1+str2,也会发生

  • str1在slot1 texture
  • str2在slot2 texture

这时label就会产生2个batchNode,那么就会触发2个drawcall

这是一个非常特殊的临界情况,label.string被存储在了2个不同的slot texture导致

所以提交渲染的逻辑就变成了

for(auto& batchNode : _batchNodes){
    QuadCommand cmd;
    renderer->addCommand(cmd);
}

有几个batchNode,就有几个quad command(因为quad command能够和triangle command合批),虽然和batchNode关联,但是很明显batchNode身上有自己的batchCommand,我不想干扰这个逻辑,顺藤摸瓜,发现和TextureAtlas关联也是个不错的主意,发现TextureAtlas也预留了一个rendererCommand的位置,很明显,原作者当初也考虑到给TextureAtlas映射一个RendererCommand,但是没实现。

经过验证,这个方法可行,已经可以顺利将多个label顺利合批,遇到一个问题,label的颜色是在uniform中定义的

uniform vec4 u_textColor;
void main()
{
    gl_FragColor =  v_fragmentColor * vec4(
        u_textColor.rgb,// RGB from uniform
        u_textColor.a * texture2D(CC_Texture0, v_texCoord).a// A from texture & uniform
    );
}

uniform的方案不支持不同的颜色,现在的顶点颜色字段,是被_displayedColor属性占用着,对应shader的v_fragmentColor,设计意图,它是一个基础颜色,和textColor不是一个概念,不能混用。

那解决办法只能把textColor的数据放到顶点数据中,但是这么做会增加顶点数据量,而且现在好像也不支持vertex format,顶点数据格式是固定的,看样子得支持下vertex format

struct CC_DLL V3F_C4B_T2F
{
    /// vertices (3F)
    Vec3     vertices;            // 12 bytes
    /// colors (4B)
    Color4B      colors;              // 4 bytes
    // tex coords (2F)
    Tex2F        texCoords;           // 8 bytes
    
    Vec4 outlineColor;// 描边颜色
    Vec4 textColor; // 字体颜色
};

如果不改动顶点颜色,那么就需要纹理带颜色,同样会带来纹理冗余的问题,需要看下unity这块是怎么实现的,应该是放到了顶点里面

FontAtlas获取发生变化:

label对应一个fontAtlas,这个fontAtlas也是从cache中获取,不同的模式对应的cache key

  • charmap:
    • plistFile
    • textureID, width, height, startChar
    • charMapFile, width, height, startChar
  • bmfont:
    • bmfontFile, imageOffset
  • ttf:
    • ttfConfig

为保证所有的字符纹理都在一个FontAtlas里面,这个key就不能以字体的特性作为基准,那么这个key应该就是一个固定的key了,因为这个FontAtlasCache只有label在使用,所以直接改造这个问题不大。

RenderCommand

TrianglesCommand

  • 顶点 polygon.triangles

描边发生了2次draw,现在得一次完成,想办法把shader的逻辑合并。

outline.a=texture_outline.a
font.a=texture_font.a
if(font.a > 0){
    // 字体纹理有颜色就使用字体纹理的颜色
    return font_color;
}else if(outline.a > 0){
    // 描边
    return outline_color;
}else{
    return null;
}

不描边时 texture1==texture2

image.png

unity

实际测试unity的字符纹理图集发现,unity会根据字符纹理数量,自动扩充字符纹理图集的大小,从512*5121024*1024再到2048*2048再到4096*4096