<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/ Game Development</title><link>https://blog.iany.me/zh/tags/game-development/</link><description>Game Development的最新内容 «~iany/»</description><language>zh-CN</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Sat, 13 May 2017 17:43:59 +0800</lastBuildDate><atom:link href="https://blog.iany.me/zh/tags/game-development/index.xml" rel="self" type="application/rss+xml"/><item><title>Cocos2D-X TTF 字体排版</title><link>https://blog.iany.me/zh/2017/05/cocos2d-x-ttf/</link><pubDate>Sat, 13 May 2017 17:43:59 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/05/cocos2d-x-ttf/</guid><description>&lt;p&gt;最近在公司的一个 Cocos2D-X 项目中碰到一个问题，TTF 文本加描边后会变宽，而且很明显没有对齐，字和描边之间没齐，每个字的水平基准线也没有对齐。最后发现是排版的代码有问题，官方分支上已经修复，但是这个项目使用的是 quick 分支出来的社区版，所以手动把修改做了个补丁提交了个&lt;a href="https://github.com/u0u0/Quick-Cocos2dx-Community/pull/76"&gt;修复TTF 描边效果的 PR&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在 Cocos2D-X 是 TTF 的排版是使用的开源库 &lt;a href="https://www.freetype.org/freetype2/docs/documentation.html"&gt;FreeType 2&lt;/a&gt;，核心的实现基本都在 &lt;code&gt;cocos/2d/CCFontFreeType.cpp&lt;/code&gt; 中的 &lt;code&gt;FontFreeType::getGlyphBitmap&lt;/code&gt;。原理是通过 FreeType 为每个字生成位图，然后通过 FreeType 返回的排版信息放到合适的位置，而问题就出现加了描边之后，位置计算的不正确。&lt;/p&gt;
&lt;p&gt;为了弄清楚原因，又去看了下相关的文档，了解了下 Cocos2D-X 具体是如何排版 TTF 的。&lt;/p&gt;
&lt;h2 id="glyph-convention"&gt;Glyph Convention&lt;/h2&gt;
&lt;p&gt;FreeType 本身不光提供的 API 文档，而且还有很丰富的文档说明字体排版中的&lt;a href="https://www.freetype.org/freetype2/docs/glyphs/index.html"&gt;各种概念&lt;/a&gt;。在排版中最小的单位是一个 Glyph，对应文本中的一个字符。Glyph 会有一个原点 (Original)，又被称为 pen cursor。在横向排版中，pen cursor 会在水平基准线上从左到右排列。&lt;/p&gt;
&lt;p&gt;Glyph 中可见像素的最小包围框被称作 Bouncing Box (略作 bbox)。Bouncing Box 左上角相对原点会有一个偏移，即 &lt;code&gt;(horiBearingX, horiBearingY)&lt;/code&gt;。这样导出位图的时候就不用包含空白了。而 &lt;code&gt;horiAdvance&lt;/code&gt; 则是下一个字符的原点相对该字符的偏移。&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="FreeType Glyph [图片参考来源](https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html)" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyph_hu_c13864d5f05e2957.png" srcset="https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyph_hu_5685fc4b4d14cf35.png 400w, https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyph_hu_c13864d5f05e2957.png 709w" sizes="(max-width: 400px) 100vw, 709px" /&gt;
&lt;figcaption &gt;FreeType Glyph &lt;a href="https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html"&gt;图片参考来源&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;要显示一行文本只需要先确定第一个字符原点的位置，相对这个位置找到 bbox 左上角的点，然后把 Glyph 位图填充在这个位置。再根据 &lt;code&gt;horiAdvance&lt;/code&gt; 确定下一个字符的原点，重复直到处理完所有字符。&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Glyphs Line" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyphs-line_hu_57a7c4722fd2f4e2.png" srcset="https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyphs-line_hu_36fc95eb58aad9bc.png 400w, https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyphs-line_hu_57a7c4722fd2f4e2.png 656w" sizes="(max-width: 400px) 100vw, 656px" /&gt;
&lt;figcaption &gt;Glyphs Line&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="stroker"&gt;Stroker&lt;/h2&gt;
&lt;p&gt;要添加描边的话，实际是每个字符用 FreeType 导出了两次位图，一次是字符本体，一次是描边。描边本身的位图是会比本体大的，如下如示。&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Glyphs 加描边" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyph-outline_hu_8c25f39bae296bac.png" srcset="https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyph-outline_hu_253ab65b49c8179c.png 400w, https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyph-outline_hu_8c25f39bae296bac.png 493w" sizes="(max-width: 400px) 100vw, 493px" /&gt;
&lt;figcaption &gt;Glyphs 加描边&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;之前的代码比较粗暴地把本体和描边中心对齐，然后也没改位图左上角相对原点的偏移。因为不同的字符描边在各个方向多出的尺寸并不是一致的，放在一起就明显会不齐了。&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Glyph 没对齐" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyph-invalid-position_hu_b1dd03580aafade8.png" srcset="https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyph-invalid-position_hu_1b8a98c5569119ad.png 400w, https://blog.iany.me/2025/09/cocos2d-x-ttf/freetype-glyph-invalid-position_hu_b1dd03580aafade8.png 493w" sizes="(max-width: 400px) 100vw, 493px" /&gt;
&lt;figcaption &gt;Glyph 没对齐&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="测试代码"&gt;测试代码&lt;/h2&gt;
&lt;p&gt;要测试的话，比较方便的是使用有 FreeType 库的脚本语言，比如 Python，下面的代码使用到了 freetype-py 来生成位图，然后使用 Pillow 生成图片。&lt;/p&gt;
&lt;p&gt;※ &lt;a href="https://github.com/doitian/freetype-label-test/blob/master/main.py"&gt;main.py&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import freetype as ft
from PIL import Image
def main(stroke=0):
&amp;quot;&amp;quot;&amp;quot;executable entry.&amp;quot;&amp;quot;&amp;quot;
face = ft.Face('./WenQuanYiMicroHei.ttf')
face.set_char_size(48*64)
if stroke == 0:
flags = ft.FT_LOAD_DEFAULT
else:
flags = ft.FT_LOAD_DEFAULT | ft.FT_LOAD_NO_BITMAP
face.load_char('心', flags)
slot = face.glyph
glyph = slot.get_glyph()
if stroke &amp;gt; 0:
stroker = ft.Stroker()
stroker.set(
64,
ft.FT_STROKER_LINECAP_ROUND,
ft.FT_STROKER_LINEJOIN_ROUND,
0
)
glyph.stroke(stroker, True)
blyph = glyph.to_bitmap(ft.FT_RENDER_MODE_NORMAL, ft.Vector(0, 0), True)
bitmap = blyph.bitmap
width, rows, pitch = bitmap.width, bitmap.rows, bitmap.pitch
print({
'width': slot.metrics.width &amp;gt;&amp;gt; 6,
'height': slot.metrics.height &amp;gt;&amp;gt; 6,
'horiBearingX': slot.metrics.horiBearingX &amp;gt;&amp;gt; 6,
'horiBearingY': slot.metrics.horiBearingY &amp;gt;&amp;gt; 6,
'horiAdvance': slot.metrics.horiAdvance &amp;gt;&amp;gt; 6,
'bitmapWidth': bitmap.width,
'bitmapHeight': bitmap.rows
})
img = Image.new(&amp;quot;L&amp;quot;, (width, rows), &amp;quot;black&amp;quot;)
pixels = img.load()
for y in range(img.size[1]):
offset = y * pitch
for x in range(img.size[0]):
pixels[x, y] = 255 - bitmap.buffer[offset + x]
img.show()
if __name__ == '__main__':
main()
main(2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整项目代码在 &lt;a href="https://github.com/doitian/freetype-label-test"&gt;GitHub - doitian/freetype-label-test&lt;/a&gt;&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/">~iany/</category><category domain="https://blog.iany.me/zh/tags/game-development/">Game Development</category><category domain="https://blog.iany.me/zh/tags/cocos2dx/">Cocos2d-x</category><category domain="https://blog.iany.me/zh/tags/typography/">Typography</category></item></channel></rss>