字体
使用的第三方库:FreeType(封装了TrueType)。cocos2dx再使用FontFreeType对其进行封装。
FontFreeType
Create
- 根据fontName读取字体文件,并缓存在
s_cacheFontData
中 - 第一次时,使用
FT_Init_FreeType( &_FTlibrary)
初始化FreeType的FT_Library
FT_Library类对应一个库的单一实例句柄,库对象是所有FT其他对象的容器,开发者需要在做任何事前创建一个新的库实例。通常客户程序通过调用FT_New_Library()方法来创建一个新的库对象。
一个应用程序通常只有一个库实例,销毁它时会销毁它的所有子对象,如face,可以通过FT_DoneFreeType方法销毁库对象,Cocos2d-x在清除Director时销毁库实例。
- 调用
FT_New_Memory_Face
初始化FT_Face
对象
if (FT_New_Memory_Face(getFTLibrary(), s_cacheFontData[fontName].data.getBytes(), s_cacheFontData[fontName].data.getSize(), 0, &face ))
return false;
一个外观对象对应单个字体外观,即一个特定风格的特定外观类型,例如 Arial和Arial Italic是两个不同的外观。
一个外观对象通常使用 FT_New_Face()来创建,这个函数接收如下参数:一个FT_Library句柄,一个表示字体文件的文件路径,一个决定从文件中装载外观的索引(一个字体文件可能有不同的外观,通常取0),以及FT_Face句柄的地址。它返回一个错误码,如果函数调用成功,则返回0,face参数将被设置成一个非NULL值。
如果已经把字体文件装载到内存,也可以简单地使用FT_New_Memory_Face来创建一个face对象。face对象包含一些用来描述全局字体数据的属性,可以被客户程序直接访问,例如外观中字形的数量、外观家族的名称、风格名称、EM大小等。
- 设置字体的字符集,如果设置UNICODE失败了,会遍历一个非FT_ENCODING_NONE的设置。
if (FT_Select_Charmap(face, FT_ENCODING_UNICODE))
{
int foundIndex = -1;
for (int charmapIndex = 0; charmapIndex < face->num_charmaps; charmapIndex++)
{
if (face->charmaps[charmapIndex]->encoding != FT_ENCODING_NONE)
{
foundIndex = charmapIndex;
break;
}
}
if (foundIndex == -1)
{
return false;
}
_encoding = face->charmaps[foundIndex]->encoding;
if (FT_Select_Charmap(face, _encoding))
{
return false;
}
}
FT_CharMap类型用来作为字符映射表对象的句柄,每个FT_CharMap对象包含一个platform和encoding属性,用来标识它对应的字符指令系统。每个字体格式都提供自己的FT_CharMapRec的继承类型并实现它们。在Cocos2d-x中统一使用Unicode编码方式。
- 设置font size
// set the requested font size
int dpi = 72;
int fontSizePoints = (int)(64.f * fontSize * CC_CONTENT_SCALE_FACTOR());
if (FT_Set_Char_Size(face, fontSizePoints, fontSizePoints, dpi, dpi))
return false;
每个FT_Face对象包含一个或多个FT_Size对象,一个size对象用来存放指定字符宽度和高度的特定数据,每个新创建的外观对象有一个尺寸,可以通过face->size直接访问。尺寸对象的内容可以通过调用FT_Set_Pixel_Sizes() 或者 FT_Set_Char_Size() 来改变。例如同样在createFontObject中使用fontSize参数来设置字体大小。
- 字形槽 FT_GlyphSlot
字形槽的目的是提供一个地方,可以很容易地一个个地装入字形映像,而不管它的格式(位图、向量轮廓或其他,FreeType也可以加载点阵位图)。理想情况是,一旦一个字形槽创建了,任何字形映像都可以装入,无须其他的内存分配。在实际中,只对于特定格式才如此,像TrueType,它显式地提供数据来计算一个槽的最大尺寸。例如在Cocos2d-x中计算字符精灵表时,使用FT_Load_Glyph来加载字形映像。
Load 字符加载 FT_Load_Char
A function used to load a single glyph into the glyph slot of a */
/* face object, according to its character code.
FT_EXPORT( FT_Error )
FT_Load_Char( FT_Face face,
FT_ULong char_code,
FT_Int32 load_flags );
其中的参数load_flags决定了加载后字体图像的属性。
这里只对常用的进行解释:
关闭抗锯齿:FT_LOAD_MONOCHROME(这个其实是加载1位bmp位图)
打开抗锯齿:FT_LOAD_FORCE_AUTOHINT
加载时就已经生成图像:FT_LOAD_RENDER
加载时不生成图像:FT_LOAD_NO_BITMAP(注意:这里需要在加载后调用FT_Render_Glyph)
unsigned char* FontFreeType::getGlyphBitmap(uint64_t theChar, long &outWidth, long &outHeight, Rect &outRect,int &xAdvance)
unsigned char* FontFreeType::getGlyphBitmap(uint64_t theChar, long &outWidth, long &outHeight, Rect &outRect,int &xAdvance)
{
bool invalidChar = true;
unsigned char* ret = nullptr;
do
{
if (_fontRef == nullptr)
break;
if (_distanceFieldEnabled)
{
if (FT_Load_Char(_fontRef, static_cast<FT_ULong>(theChar), FT_LOAD_RENDER | FT_LOAD_NO_HINTING | FT_LOAD_NO_AUTOHINT))
break;
}
else
{
if (FT_Load_Char(_fontRef, static_cast<FT_ULong>(theChar), FT_LOAD_RENDER | FT_LOAD_NO_AUTOHINT))
break;
}
auto& metrics = _fontRef->glyph->metrics;
outRect.origin.x = static_cast<float>(metrics.horiBearingX >> 6);
outRect.origin.y = static_cast<float>(-(metrics.horiBearingY >> 6));
outRect.size.width = static_cast<float>((metrics.width >> 6));
outRect.size.height = static_cast<float>((metrics.height >> 6));
xAdvance = (static_cast<int>(_fontRef->glyph->metrics.horiAdvance >> 6));
outWidth = _fontRef->glyph->bitmap.width;
outHeight = _fontRef->glyph->bitmap.rows;
ret = _fontRef->glyph->bitmap.buffer;
if (_outlineSize > 0 && outWidth > 0 && outHeight > 0)
{
auto copyBitmap = new (std::nothrow) unsigned char[outWidth * outHeight];
memcpy(copyBitmap,ret,outWidth * outHeight * sizeof(unsigned char));
FT_BBox bbox;
auto outlineBitmap = getGlyphBitmapWithOutline(theChar,bbox);
if(outlineBitmap == nullptr)
{
ret = nullptr;
delete [] copyBitmap;
break;
}
int glyphMinX = (int)outRect.origin.x;
int glyphMaxX = (int)(outRect.origin.x + outWidth);
int glyphMinY = (int)(-outHeight - outRect.origin.y);
int glyphMaxY = (int)-outRect.origin.y;
auto outlineMinX = bbox.xMin >> 6;
auto outlineMaxX = bbox.xMax >> 6;
auto outlineMinY = bbox.yMin >> 6;
auto outlineMaxY = bbox.yMax >> 6;
auto outlineWidth = outlineMaxX - outlineMinX;
auto outlineHeight = outlineMaxY - outlineMinY;
auto blendImageMinX = MIN(outlineMinX, glyphMinX);
auto blendImageMaxY = MAX(outlineMaxY, glyphMaxY);
auto blendWidth = MAX(outlineMaxX, glyphMaxX) - blendImageMinX;
auto blendHeight = blendImageMaxY - MIN(outlineMinY, glyphMinY);
outRect.origin.x = (float)blendImageMinX;
outRect.origin.y = -blendImageMaxY + _outlineSize;
unsigned char *blendImage = nullptr;
if (blendWidth > 0 && blendHeight > 0)
{
FT_Pos index, index2;
auto imageSize = blendWidth * blendHeight * 2;
blendImage = new (std::nothrow) unsigned char[imageSize];
memset(blendImage, 0, imageSize);
auto px = outlineMinX - blendImageMinX;
auto py = blendImageMaxY - outlineMaxY;
for (int x = 0; x < outlineWidth; ++x)
{
for (int y = 0; y < outlineHeight; ++y)
{
index = px + x + ((py + y) * blendWidth);
index2 = x + (y * outlineWidth);
blendImage[2 * index] = outlineBitmap[index2];
}
}
px = glyphMinX - blendImageMinX;
py = blendImageMaxY - glyphMaxY;
for (int x = 0; x < outWidth; ++x)
{
for (int y = 0; y < outHeight; ++y)
{
index = px + x + ((y + py) * blendWidth);
index2 = x + (y * outWidth);
blendImage[2 * index + 1] = copyBitmap[index2];
}
}
}
outRect.size.width = (float)blendWidth;
outRect.size.height = (float)blendHeight;
outWidth = blendWidth;
outHeight = blendHeight;
delete [] outlineBitmap;
delete [] copyBitmap;
ret = blendImage;
}
invalidChar = false;
} while (0);
if (invalidChar)
{
outRect.size.width = 0;
outRect.size.height = 0;
xAdvance = 0;
return nullptr;
}
else
{
return ret;
}
}
绘制
遍历text的所有字符,调用以下流程(不同text无法合批):
- 调用
getGlyphBitmap
获取单个字符的bmp数据
auto bitmap = _fontFreeType->getGlyphBitmap(it.second, bitmapWidth, bitmapHeight, tempRect, tempDef.xAdvance);
- 设置宽高偏移后调用
renderCharAt
将数据复制给_currentPageData
_fontFreeType->renderCharAt(_currentPageData, (int)_currentPageOrigX + adjustForExtend, (int)_currentPageOrigY + adjustForExtend, bitmap, bitmapWidth, bitmapHeight);
- 在
updateTextureContent
接口中,使用Texture2D
的updateWithData
接口更新数据
_atlasTextures[_currentPage]->updateWithData(data, 0, startY, CacheTextureWidth, (int)_currentPageOrigY - startY + _currLineHeight);
- 调用
glTexSubImage2D
上传数据
void Texture2DGL::updateSubData(std::size_t xoffset, std::size_t yoffset, std::size_t width, std::size_t height, std::size_t level, uint8_t* data)
{
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _textureInfo.texture);
glTexSubImage2D(GL_TEXTURE_2D,
level,
xoffset,
yoffset,
width,
height,
_textureInfo.format,
_textureInfo.type,
data);
CHECK_GL_ERROR_DEBUG();
if(!_hasMipmaps && level > 0)
_hasMipmaps = true;
}
特效
- 阴影(Shadow)是3种特效里实现最简单的一种,同时也是性能最差的一种,它仅将Label分别使用阴影颜色和字体颜色单独绘制一次,参见如下所示的Label的绘制方法。
if (_shadowNode)
{
_shadowNode->visit(renderer, _modelViewTransform, flags);
}
_textSprite->visit(renderer, _modelViewTransform, flags);
- 描边(outline)
Cocos2d-x中的描边(Outline)仅支持轮廓字体,因为它是借助于FreeType 的
FT_Stroker模块来实现的。
FT_Stroker 能够给字形生成一些轮廓信息,并且提供接口判断是否位于轮廓内或者轮廓外。在Cocos2d-x中,如果对Label使用了描边特效,则FontFreeType在初始化的时候会设置FT_Stroker相关的内容。
FontFreeType::FontFreeType(bool distanceFieldEnabled /* = false */, float outline /* = 0 */)
: _fontRef(nullptr)
, _stroker(nullptr)
, _encoding(FT_ENCODING_UNICODE)
, _distanceFieldEnabled(distanceFieldEnabled)
, _outlineSize(0.0f)
, _lineHeight(0)
, _fontAtlas(nullptr)
, _usedGlyphs(GlyphCollection::ASCII)
{
if (outline > 0.0f)
{
_outlineSize = outline * CC_CONTENT_SCALE_FACTOR();
FT_Stroker_New(FontFreeType::getFTLibrary(), &_stroker);
FT_Stroker_Set(_stroker,
(int)(_outlineSize * 64),
FT_STROKER_LINECAP_ROUND,
FT_STROKER_LINEJOIN_ROUND,
0);
}
}
然后使用getGlyphBitmapWithOutline来获取轮廓信息,其中使用FT_Glyph_StrokerBorder来获取FT_Outline对象,FT_Outline 中则包含了轮廓信息。
与一般情况下使用一个A8格式的纹理来保存一个单通道的Alpha值不同,描边需
要同时保存字体区域的Alpha值和描边区域的Alpha值,在Cocos2d-x中使用一个AI88的纹理来保存带有描边信息的字符精灵表。
在 AI88纹理中,其中一个通道用来保存描边区域,而另一个通道保存字体区域。
FT_Outline包含的区域实际上是包括描边轮廓和字体在内的所有区域,如果用一个单通道的灰度图来表示FT_Outline,则它可以用图13.19上图表示。另一个通道则用来保存字体本身的Alpha通道,它的灰度图如图13.19下图所示。注意,通过对比,同样字号下FT_Outline所占的区域比单独的字体区域要大,这部分用来放置描边区域。
这样,在片段着色器中,描边轮廓内字体区域的Alpha通道值为0,而描边区域的
Alpha通道值为1,可以以此来区分描边区域和字体区域。如下代码是在Cocos2d-x中Label对描边特效使用的片段着色器:
/****************************************************************************
Copyright (c) 2018-2019 Xiamen Yaji Software Co., Ltd.
http://www.cocos2d-x.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
/*
* LICENSE ???
*/
const char* labelOutline_frag = R"(
#ifdef GL_ES
precision lowp float;
#endif
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform vec4 u_effectColor;
uniform vec4 u_textColor;
uniform sampler2D u_texture;
#ifdef GL_ES
uniform lowp int u_effectType; // 0: None (Draw text), 1: Outline, 2: Shadow
#else
uniform int u_effectType;
#endif
void main()
{
vec4 sample = texture2D(u_texture, 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);
}
}
)";
在该算法中,纹理的A通道表示字体 Alpha值,而R通道表示描边区域Alpha值,
color变量为字体颜色与描边颜色混合后的值。在字体区域,因为字体的透明度为1,所以描边区域不可见。
描边也是增加了renderCommand,使用使用上面的shader,设置不同参数再次绘制
case LabelEffect::OUTLINE:
{
int effectType = 0;
Vec4 effectColor(_effectColorF.r, _effectColorF.g, _effectColorF.b, _effectColorF.a);
//draw shadow
if(_shadowEnabled)
{
effectType = 2;
Vec4 shadowColor = Vec4(_shadowColor4F.r, _shadowColor4F.g, _shadowColor4F.b, _shadowColor4F.a);
auto *programStateShadow = batch.shadowCommand.getPipelineDescriptor().programState;
programStateShadow->setUniform(_effectColorLocation, &shadowColor, sizeof(Vec4));
programStateShadow->setUniform(_effectTypeLocation, &effectType, sizeof(effectType));
batch.shadowCommand.init(_globalZOrder);
renderer->addCommand(&batch.shadowCommand);
}
//draw outline
{
effectType = 1;
updateBuffer(textureAtlas, batch.outLineCommand);
auto *programStateOutline = batch.outLineCommand.getPipelineDescriptor().programState;
programStateOutline->setUniform(_effectColorLocation, &effectColor, sizeof(Vec4));
programStateOutline->setUniform(_effectTypeLocation, &effectType, sizeof(effectType));
batch.outLineCommand.init(_globalZOrder);
renderer->addCommand(&batch.outLineCommand);
}
//draw text
{
effectType = 0;
auto *programStateText= batch.textCommand.getPipelineDescriptor().programState;
programStateText->setUniform(_effectColorLocation, &effectColor, sizeof(effectColor));
programStateText->setUniform(_effectTypeLocation, &effectType, sizeof(effectType));
}
}
- 发光
void main()
{
float dist = texture2D(u_texture, v_texCoord).a;
//TODO: Implementation 'fwidth' for glsl 1.0
//float width = fwidth(dist);
//assign width for constant will lead to a little bit fuzzy,it's temporary measure.
float width = 0.04;
float alpha = smoothstep(0.5-width, 0.5+width, dist);
//glow
float mu = smoothstep(0.5, 1.0, sqrt(dist));
vec4 color = u_effectColor*(1.0-alpha) + u_textColor*alpha;
gl_FragColor = v_fragmentColor * vec4(color.rgb, max(alpha,mu)*color.a);
}
与前面使用Distance Field的片段着色器一样,0.54 以上的区域Alpha值为1,将取
字体本身的颜色,而0~0.5的部分则用来表示发光颜色,用字体颜色混合发光颜色,最
终呈现出正常的发光效果。
架构
Label是对所有字体的封装,实际使用通过FontAltasCache
管理FontAltas
。FontAtlasCache使用字体size,描边size,是否使用distanceField作为一个FontAltas的key。
snprintf(keyPrefix, ATLAS_MAP_KEY_PREFIX_BUFFER_SIZE, useDistanceField ? "df %.2f %d " : "%.2f %d ", config->fontSize, config->outlineSize);
从上述代码中可以看出,选择一个不同的字体尺寸会导致FontAtlas的重新创建,所以,应用程序中不应该出现多种差别很小的字体尺寸,这会导致多个字体精灵内存的占用。
FontAtlas* FontAtlasCache::getFontAtlasTTF(const _ttfConfig* config)
{
auto realFontFilename = FileUtils::getInstance()->getNewFilename(config->fontFilePath); // resolves real file path, to prevent storing multiple atlases for the same file.
bool useDistanceField = config->distanceFieldEnabled;
if(config->outlineSize > 0)
{
useDistanceField = false;
}
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);
if ( it == _atlasMap.end() )
{
auto font = FontFreeType::create(realFontFilename, config->fontSize, config->glyphs,
config->customGlyphs, useDistanceField, (float)config->outlineSize);
if (font)
{
auto tempAtlas = font->createFontAtlas();
if (tempAtlas)
{
_atlasMap[atlasName] = tempAtlas;
return _atlasMap[atlasName];
}
}
}
else
return it->second;
return nullptr;
}
distance field
Distance filed fonts
讲一讲一种新型的字体渲染方式
Distance Field每个像素记录的不是该像素的Alpha通道值,而是每个像素到文字边缘的距离,例如:
- 文字区域外某点的值是它到离它最近的文字区域的距离,这个距离的取值范围
为0~0.5。 - 文字区域内某点的值是它到离它最近的非文字区域的距离,这个距离的取值范
围为0.5~1.0。 - 文字边缘的值为0.5。
有了这个图的数据之后,应用程序就可以使用两种方法来绘制文字。第一种方法使用AIpha测试,这种方法只能在桌面版OpenGL中使用。对渲染管线设置当Alpha 大于0.5时绘制,否则丢弃。由于此时实际渲染并不依赖于图形管线对纹理的缩放,而只是取其缩放后的轮廓(大于0.5的区域),其颜色值不受硬件缩放的影响,所以可以呈现较高的清晰度。但是由于轮廓被线性插值,则会导致一些锯齿。
另一种方法使用自定义的片段着色器来绘制,在片段着色器中可以使用smoothstep方法来使轮廓更平滑
而且因为源数据是离边的距离,两个像素的插值就是当前像素离边的距离。
这个信息是线性的,插值数据不会偏离太远,抗缩放性能非常好。