我们以Label为例,Cocos2d-x 提供的 Label 对象可以使用位图字体,TrueType 字体,系统字体创建。
tips:
enableWrap(true) 当文本长度大于Label设置width后会自动换行
BMFont
BMFont 是一个使用位图字体创建的标签类型,位图字体中的字符由点阵组成。标签中的每一个字符都是一个单独的 Sprite,也就是说精灵的属性(旋转,缩放,着色等)控制都适用于这里的每个字符。创建 BMFont 标签需要两个文件:.fnt 文件和 .png 文件。其中.png 文件是一张合图,你需要显示的字符都应该在这里找到,而.fnt 则是字体的描述文件,它还保存了各个字符的信息。cocos 的 tests 里有提供这种字体,其中bitmapFontTest2.png、bitmapFontTest2.fnt的内容预览如下:
描述文件是在BMFontConfiguration::parseConfigFile() 中解析的:
page id
行指定了字体的.png文件char
行 是单个字符的信息kerning
行是first字符和second字符之间的水平间距调整的值
而纹理文件(png)加载后由FontAtlas维护。
下边理一下 BMFont的渲染流程:
- 加载字体文件,包括字体纹理和基本配置以及单个字符的信息(在纹理中的坐标位置、尺寸、布局时坐标偏移等)。
- 拆分字符串,计算每个字符的位置、大小等。这里的字符的大小调整是通过缩放纹理来实现的,所以 BMFont在设置非默认的字体大小会因为缩放导致失真。Label::multilineTextWrap() 函数里会进行自动换行(空白字符换行不拆分单词等)、特殊字符(\n,\b,\r等)的处理。
- 在函数 updateQuads() 中通过 SpriteBatchNode 收集所有字符纹理的渲染数据,在渲染流程中绘制出来
TTF
TTF是一种是矢量字体,并不会像BMF字体那样随着字号的调整而出现模糊失真,cocos 使用FreeType 来处理这一块的逻辑,关于矢量字体和FreeType 的介绍和使用可以参考网友的Freetype 介绍和使用。
在使用TTF的Label 设置好字体大小(字号)后,会通过_fontAtlas->prepareLetterDefinitions(_utf32Text);
为将要显示的文本的各个字符生成纹理并缓存在_fontAtlas中。所以当字号修改了之后,就需要重新生成对应字号的纹理:
FontAtlas* FontAtlasCache::getFontAtlasTTF(const _ttfConfig* config)
{
... ...
std::string key;
char keyPrefix[ATLAS_MAP_KEY_PREFIX_BUFFER_SIZE];
snprintf(keyPrefix, ATLAS_MAP_KEY_PREFIX_BUFFER_SIZE, useDistanceField ? "df %.2f %d " : "%.2f %d ", config->fontSize, config->outlineSize);
std::string atlasName(keyPrefix);
atlasName += realFontFilename;
auto it = _atlasMap.find(atlasName);
... ...
}
可以看到 TTF 的 FontAtlas 缓存的key跟 文件名、字号、描边宽度是相关的
TTF 的渲染流程:
1.通过FontFreeType加载ttf字体
2.根据当前文本、文本的字号、描边的宽度为所有字符生成纹理,后边就跟BMF相同了
3. 拆分字符串,计算每个字符的位置、大小等
4. 在函数 updateQuads() 中通过 SpriteBatchNode 收集所有字符纹理的渲染数据,在渲染流程中绘制出来
TTF 还额外支持描边(outline)和发光效果(glow) 。
描边效果是通过FreeType 重新生成带描边的纹理。发光效果就比较有意思了,用到了距离场(DistanceField)来描述字体,它预处理字体的图像来产生一个有符号距离场。比如字体图像为:
这是一个位图,每个像素的值在0-1之间,其中0代表黑色,1代表白色。我们把它转换成距离场表示,黑色区域中与白色相邻的地方称为边界,记为0.5,黑色的其他像素则根据自身距最近边界距离计算一个值 [0-0.5) 之间,距离越远,值越大,比如距最近边界超过10像素的记为0,小于10的则按照距离计算:0.5-distance/10;白色区域同理,根据自身像素距边界最近的距离计算一个值 (0.5-1] 之间,距最近边界超过10像素的记为0,小于10的则按照距离计算:0.5+distance/10。如果我们将这个距离场直接渲染出来(1表示无色:黑,0表示白色,其他则表现为不同的灰色)则是类似于这样的一张图:
如果我们把小于0.5的像全部丢弃,大于0.5的绘制成白色能得到原始的图形;如果我们将0.5-0.8的像素渲染成指定的颜色,就可以得到指定颜色的外发光效果了。
这里有篇文件讲的比较清楚 讲一讲一种新型的字体渲染方式。
系统字体
采用系统字体的Label整体渲染流程与TTF字体比较类似,提供文本,然后通过原生接口生成纹理,再渲染到屏幕上。
生成纹理的接口是:
bool Texture2D::initWithString(const char *text, const FontDefinition& textDefinition)
通用的特殊效果的实现
阴影的实现比较简单,先将自身设置为阴影的颜色、加上阴影的位置偏移 绘制一遍(先绘制所以垫在原字形的下边,代码位于Label::onDrawShadow()内),然后恢复之前的状态,再绘制原文本(代码位于Label::onDraw()内)。粗体的实现与阴影相同:
void Label::enableBold()
{
if (!_boldEnabled)
{
// bold is implemented with outline
enableShadow(Color4B::WHITE, Size(0.9f, 0), 0);
// add one to kerning
setAdditionalKerning(_additionalKerning+1);
_boldEnabled = true;
}
}
删除线和下划线是通过一个 _underlineNode = DrawNode::create();
子节点来绘制。
倾斜 则是通过设置 SkewX 来修改绘制时的投影矩阵。