文字是怎么渲染的?
前言
“文字是如何渲染的?” 这个看似简单的问题,往往被我们忽略。每天,我们与屏幕上的文字打交道,输入、阅读,流畅得如同呼吸。然而,很少有人停下来思考:屏幕上清晰呈现的每一个字符背后,究竟隐藏着怎样精密而复杂的技术链条?
这篇博客并非要深入探讨晦涩难懂的专业知识,而更像是一份探索笔记,忠实记录我试图解开这一“简单”谜题的思考轨迹、所学知识,以及意外发现的有趣联系。希望与你一同踏上这段从“习以为常”到“恍然大悟”的旅程。
开发环境
Mac M1 VSCode + CMake OpenGL AI 工具:DeepSeek、Copilot、Gemini 其它工具:Figma 、geogebra 科学计算器、Wolfram Mathematica 字体:FreeSerifItalic.ttf、AlibabaPuHuiTi-3-55-Regular.ttf文字渲染
先来问问 AI,看能不能直接得到满意的答案。
文字是怎么渲染的?
文字渲染是将字符从编码转换为屏幕上的可视图像的过程,涉及多个技术环节。以下是其核心流程和关键技术的分步解析:
- 字体解析:从字符到轮廓
- 字体文件:字体(如TTF、OTF)存储了每个字符的矢量轮廓(由贝塞尔曲线构成)和元数据(字距、行高等)。
- 字符映射:根据编码(如Unicode)找到对应字形(Glyph),例如字母"A"的矢量路径。
- Hinting(微调):针对低分辨率屏幕调整轮廓,优化小字号的显示清晰度。
- 矢量到像素:栅格化与抗锯齿
- 栅格化:将矢量轮廓转换为位图。关键问题是如何在像素网格中填充曲线边缘。
- 抗锯齿技术:
- 灰度抗锯齿:用不同灰度的像素模拟平滑边缘(如:A的边缘像素显示为浅灰色)。
- 子像素渲染(如ClearType):利用LCD屏幕的RGB子像素排列,横向精度提高3倍(微软专利技术)。
- Mac的字体平滑:侧重保持字形原始形状,可能牺牲部分锐度。
- 合成与颜色处理
- 颜色叠加:字形位图通常为单通道(Alpha通道),与设定的文字颜色(如#FF0000)相乘,生成带颜色的文字图像。
- 混合背景:根据透明度(Alpha)与背景像素混合,例如半透明文字叠加在图片上。
- 高级渲染技术
- GPU加速:利用显卡并行计算加速栅格化和混合(如浏览器中的WebGL渲染)。
- 矢量保留渲染(PDF/SVG):直接绘制轮廓,缩放不失真。
- 多语言支持:复杂文本布局(如阿拉伯语连字、竖排中文)。
- 平台差异与挑战
- Windows vs. macOS:ClearType(子像素) vs. Quartz(灰度+高分辨率优化)。
- 浏览器差异:Chrome和Firefox可能对同一字体渲染效果不同。
- 高分屏适配:Retina或4K屏幕需要2倍/3倍分辨率位图避免模糊。
常见问题与优化
- 小字号模糊:启用Hinting、选择屏幕优化字体(如微软雅黑)。
- 性能瓶颈:缓存渲染结果(如游戏中的文字纹理图集)。
- 自定义渲染:游戏引擎(Unity/Unreal)可能用Signed Distance Field(SDF)技术动态生成平滑文字。
总结流程
编码 → 字体解析 → 轮廓调整(Hinting) → 栅格化(抗锯齿) → 颜色合成 → 最终像素通过理解这些步骤,开发者可以针对不同场景优化文字显示效果(如选择渲染技术、调整字体参数),而设计师能更好地预见到多平台下的显示差异。
文字是怎么渲染的?
文字渲染是一个复杂但 fascinating 的过程,它将抽象的字符代码(比如你输入的字母)转换成屏幕上可见的像素。这个过程可以大致分为以下几个主要步骤:
- 获取字体数据 (Font Data Acquisition):
- 字体文件: 计算机上的文字显示依赖于字体文件(例如 .ttf, .otf, .woff 等格式)。这些文件包含了字符的形状描述(通常是矢量轮廓,如贝塞尔曲线,或者位图)、字符间距信息 (kerning)、字符映射表 (charmap) 以及其他元数据。
- 字体选择: 应用程序或操作系统根据需要显示的文本内容(如字体家族、字重、字号、样式等)选择合适的字体文件。
- 字体解析与加载 (Font Parsing and Loading):
- 库的作用 (如 FreeType): 像 FreeType 这样的字体渲染引擎库被用来读取和解析字体文件的复杂结构。
- 数据提取: FreeType 会从字体文件中提取必要的信息,例如:
- 字符映射表 (Charmap): 将 Unicode 字符码点(如 'A' 的码点是 U+0041)映射到字体内部的字形索引 (glyph index)。每个字形是字符的一个具体视觉表现。
- 字形数据 (Glyph Data): 根据字形索引,加载该字形的轮廓数据(点坐标、曲线控制点等)。
- 度量信息 (Metrics): 包括字符的宽度、高度、基线位置、字距调整值 (kerning pairs) 等。
- TrueType 指令 (Hinting Instructions - 如果是 TTF): 这些是嵌入在字体中的特殊指令,用于优化字形在低分辨率屏幕上的显示效果。
- 字形处理 (Glyph Processing):
- 缩放 (Scaling): 将从字体文件加载的矢量轮廓数据从字体设计单位(font units 或 EM-square)缩放到用户请求的尺寸(例如,以磅或像素为单位)。
- 轮廓暗示/微调 (Hinting): 这是一个非常关键的步骤,尤其对于低分辨率显示。
- 目的: 调整字形轮廓上的点,使其更好地对齐到像素网格上,从而避免模糊、笔画粗细不均等问题,使文字更清晰、易读。
- TrueType 指令执行: 对于 TrueType 字体,FreeType 会执行其内置的 TrueType 指令解释器来运行字体中包含的暗示指令。
- 自动暗示 (Autohinting): 如果字体没有高质量的内置指令,或者用户选择,FreeType 也可以应用其自动暗示算法来尝试优化字形。
- 变换 (Transformation): 如果需要,可以对字形轮廓应用额外的仿射变换,如旋转、倾斜等。
- 栅格化 (Rasterization):
- 矢量到像素的转换: 栅格化是将经过缩放和暗示处理的矢量字形轮廓转换为位图 (bitmap) 的过程。位图是一个由像素组成的二维网格,每个像素有特定的颜色或灰度值。
- 填充规则: 决定轮廓内部哪些像素应该被填充。
- 抗锯齿 (Anti-aliasing): 为了使文字边缘看起来更平滑,而不是锯齿状,通常会采用抗锯齿技术。这涉及到计算每个像素被字形轮廓部分覆盖的程度,并根据覆盖比例赋予该像素一个中间的颜色值(通常是不同级别的灰色,如果是彩色文本则是前景和背景色的混合)。gray_convert_glyph 这样的函数就是用于灰度抗锯齿栅格化的核心部分,它计算每个像素的覆盖信息。
- 渲染模式:
- 单色 (Monochrome/Binary): 每个像素要么是前景要么是背景,没有中间色。边缘会有锯齿。
- 灰度 (Grayscale): 使用不同级别的灰色来平滑边缘。
- 亚像素渲染 (Subpixel Rendering): 例如 ClearType (Windows) 或 FreeType 支持的 LCD 优化渲染。利用 LCD 屏幕上红、绿、蓝 (RGB) 亚像素独立寻址的特性,通过在水平方向上对亚像素进行不同的亮度控制,从而在人眼中产生更清晰、色彩更丰富的文字边缘。这通常需要知道屏幕的亚像素排列顺序。
- 位图显示 (Bitmap Display/Composition):
- 获取位图数据: 栅格化后,应用程序会得到一个包含像素数据的位图缓冲区,以及位图的尺寸、每个像素的格式(如8位灰度、32位BGRA等)和相对于绘制原点的偏移量(bitmap_left, bitmap_top)。
- 颜色与混合: 根据文本颜色和背景颜色,将栅格化后的位图(通常是灰度图或alpha蒙版)与背景进行混合。
- 文本布局: 对于一段文本,需要根据每个字形的 پیشروی宽度 (advance width) 来确定下一个字形的绘制位置。还需要考虑行高、字间距、行间距等排版信息。
- 绘制到屏幕: 最终,计算出的像素数据被发送到图形硬件,显示在屏幕上。
总结来说,文字渲染是一个从抽象字符到具体像素的转换管道,涉及到字体文件的解析、字形轮廓的几何处理、为了清晰度进行的微调(暗示),以及最终将矢量形状转换为像素表示(栅格化)。FreeType 这样的库在其中的多个阶段都扮演着至关重要的角色,尤其是字体解析、字形加载、TrueType 指令执行和栅格化准备。
💡 可以同时询问多个 AI 助手,选取一个最满意的答案,通常,我更喜欢 deepseek 的回答,但无奈经常遇到“服务器繁忙,请稍后再试”。
通过这些回答,大致了解到文字渲染的一般流程:字体解析 → 字形处理 → 栅格化 → 显示。
随后,我尝试网上搜索相关主题的博客,这类内容少之又少。在 medium.com 上找到一篇 Easy Scalable Text Rendering on the GPU,它与上述流程完美契合。然而,新问题随之而来——这篇文章虽然详尽,但对我这个没有相关背景知识的读者来说,理解起来极为困难。
此外,该博客的源码 evanw/theta 采用前端技术,并使用小众的 Skew 编程语言和 GLSLX 编译器来生成 JavaScript 代码。为了弄清楚其中的内容,只是学习这些编程语言就需要投入大量时间,而我担心在真正掌握它们之前,就已经丧失了探索的兴趣。因此,我决定沿用该博客的思路,但使用自己熟悉的开发语言来探索这个问题。
💭 在开发过程中,我们常常会遇到需要学习其他开发语言的情况,我个人一直认为,成为某个领域的专家往往比成为多个领域的通才更有价值。不过,当遇到这种需要学习新技能的场景时,可以先将问题积攒起来。我在《如何学习 C++》中提到过一个学习策略,即积攒问题。例如,我最近遇到了一些具体的需要学习 python 的场景:为了实现国际化功能,我需要写一些 Python 脚本;写博客时,偶尔需要用 Manim 来演示数学公式的推导过程;在制作一个动画机器人时,又需要用到 Python 编译一个情绪识别的小模型。当这些问题积攒得越来越多时,系统地学习 Python 就变得足够值得了。 同样,我们也可以积攒自己的想法。比如,列出最想去的十个地方,或者最想实现的十个软件功能。有了这些想法,潜意识里更加渴望拥有可自由支配的时间,这种渴望甚至会远远超过对未来失业的焦虑。
NanoSVG 解析 SVG 文件
TrueType 简介
TrueType 是一种由苹果公司(Apple)和微软(Microsoft)于 1980 年代末 联合开发的数字化字体标准,旨在提供高质量的矢量字体技术。它通过数学公式(曲线描述)定义字符轮廓,解决了传统位图字体在缩放时产生的锯齿问题,同时与 Adobe 的 PostScript Type 1 字体竞争。
核心技术特点
- 矢量轮廓
使用二次贝塞尔曲线(Quadratic Bézier Curves)描述字符形状,支持无限缩放且保持平滑边缘,适应不同分辨率设备(屏幕、打印机等)。
- 跨平台兼容性
广泛支持 Windows、macOS、Linux 等操作系统,并成为早期互联网和办公软件(如 Word)的默认字体格式之一。
- Hinting 技术
内嵌“微调指令”(Hinting),优化小字号或低分辨率下的显示效果,通过调整关键点位置提升可读性。
- 灵活性与扩展性
- 支持 TrueType Collection(.ttc),单文件包含多套字体(如多语言变体)。
- 允许嵌入文档中,确保跨设备显示一致性(如 PDF 文件)。
与其他字体格式对比
- PostScript Type 1(Adobe)
TrueType 直接集成到操作系统渲染引擎,无需额外软件;Type 1 依赖 PostScript 解释器,多用于专业印刷。
- OpenType(.otf/.ttf)
OpenType 是 TrueType 的扩展,兼容 PostScript 和 TrueType 轮廓,支持更复杂的排版功能(如连字、多语言字符)。
应用场景
- 屏幕显示:因 Hinting 技术,适合 UI 设计、网页字体等需要清晰小字号的场景。
- 基础打印:日常文档打印,但高精度印刷仍倾向 PostScript 或 OpenType-CFF。
- 文件共享:嵌入文档时保持格式稳定,减少字体缺失问题。
文件格式
- .ttf:单字体文件。
- .ttc:多字体集合文件(如中文字体包含简繁体)。
现状
尽管 OpenType 逐渐成为主流(支持更多特性),TrueType 因简单、兼容性强,仍在基础应用、Web 字体(如部分 Google Fonts)和移动端保持广泛使用。 TrueType 的推出降低了高质量字体的技术门槛,推动了数字化排版和跨平台内容创作的普及。
TrueType 原理
TrueType 是一种广泛使用的矢量字体格式,其核心原理基于数学描述和智能渲染技术,确保字体在不同尺寸和分辨率下保持清晰。以下是其工作原理的详细分点解释:
1. 矢量轮廓描述
- 数学曲线定义:TrueType 使用二次贝塞尔曲线(由起点、终点和一个控制点定义)描述字符轮廓。相较于三次贝塞尔曲线(如PostScript Type 1),二次曲线计算更高效,适合早期硬件。
- 轮廓构成:每个字形由闭合的路径(直线和曲线段)组成,路径由控制点(On-curve和Off-curve点)定义。On-curve点是轮廓的实际点,Off-curve点控制曲线形状。
2. 栅格化与Hinting技术
- 栅格化过程:将矢量轮廓转换为屏幕或打印的像素时,TrueType通过扫描线算法确定哪些像素被填充。抗锯齿技术(如灰度渲染)平滑边缘。
- Hinting(微调):关键优化步骤,通过嵌入的hinting指令调整轮廓,确保小字号下的清晰度。例如:
- 对齐像素网格:垂直或水平笔画对齐像素边界,避免模糊。
- 控制笔画粗细:调整曲线使笔画宽度均匀,防止过细或过粗。
- 消除舍入误差:优化控制点坐标,减少缩放时的失真。
3. 文件结构与数据表
TrueType字体文件(.ttf)由多个数据表组成,各司其职:
- glyf表:存储字形轮廓数据(控制点坐标、轮廓指令)。
- cmap表:字符编码到字形索引的映射(如Unicode到字形ID)。
- head表:全局信息(字体版本、修改时间、坐标范围等)。
- hhea/hmtx表:水平排版度量(字宽、左右间距)。
- maxp表:字形相关数据上限(最大点数、轮廓数等)。
- loca表:字形数据在glyf表中的位置索引。
- 其他表:支持高级功能(如OpenType的GSUB/GPOS表处理连字和字距)。
4. 动态调整与排版特性
- 缩放无关性:数学描述确保任意缩放时保持平滑,避免位图字体的锯齿问题。
- 字距调整(Kerning):调整特定字符对的间距(如“AV”),提升视觉流畅度。
- 连字(Ligatures):特定字符组合替换为连字字形(如“fi”合并为单一字形),需依赖OpenType扩展。
5. 跨平台渲染差异
- 渲染引擎差异:不同操作系统(如Windows、macOS)的TrueType渲染引擎可能对hinting指令处理不同,导致显示效果差异。
- 抗锯齿策略:部分系统默认启用次像素渲染(如ClearType),利用LCD像素结构增强锐度。
6. 设计工具与制作流程
- 字体设计软件:使用FontLab、Glyphs等工具设计轮廓并添加hinting指令。
- 控制点优化:设计师手动调整控制点,平衡曲线精度与渲染性能。
- Hinting复杂度:精细的hinting需大量手工工作,是高质量字体的关键。
总结
TrueType 通过数学曲线定义字形轮廓,结合智能栅格化与hinting技术,解决了矢量字体在小尺寸下的显示难题。其模块化的文件结构和跨平台兼容性使其成为数字排版的基础技术之一。随着技术进步,TrueType 的功能已通过OpenType等格式扩展,但核心原理仍影响现代字体设计。
💭 TrueType 这个数字化字体标准的诞生,是否是乔布斯在斯坦福大学演讲中提到的退学后参加书法课直到后来应用到个人电脑中的现实写照?
效仿 Easy Scalable Text Rendering on the GPU 博客,使用 FreeSerifItalic.ttf 字体中的字符 ‘e’ 作为示例,可以从字体在线编辑软件导出字符 e.svg 文件。

导出的 e.svg 文件其实是一个 XML 文件。
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg id="e" width="128" height="128" style="width:128px;height:128px;" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"> <path fill="#666" d="M801.99 734.51 l26.55 26.55 q-97.35 112.83 -219.03 175.88 q-121.68 63.06 -238.93 63.06 q-130.54 0 -211.29 -84.07 q-80.75 -84.07 -80.75 -219.03 q0 -161.5 99.55 -325.22 q99.55 -163.72 255.53 -267.7 q155.98 -103.98 310.85 -103.98 q81.86 0 129.42 39.83 q47.57 39.83 47.57 110.61 q0 143.81 -159.29 256.64 q-159.29 112.83 -407.08 148.23 l-61.95 8.85 q-22.12 88.49 -22.12 137.17 q0 90.71 56.42 144.91 q56.42 54.2 151.55 54.2 q70.8 0 137.17 -34.29 q66.37 -34.29 185.84 -131.64 ZM308.63 515.49 q119.47 -28.76 209.07 -70.8 q89.6 -42.04 137.16 -81.86 q47.56 -39.82 76.33 -86.28 q28.77 -46.46 36.51 -77.44 q7.74 -30.98 7.74 -61.95 q0 -37.61 -21.02 -61.95 q-21.01 -24.34 -56.41 -24.34 q-84.07 0 -190.26 109.52 q-106.19 109.52 -161.5 257.75 l-37.61 97.35 Z"/></svg>花几分时间了解一下 SVG 菜鸟教程,TureType 字体中只用到直线和二次贝塞尔曲线描述轮廓,其中,e.svg 解析规则很简单,M:移动 L:直线 Q:二次贝塞尔曲线,字母大写表示绝对坐标,字母小写表示相对坐标, 二次贝塞尔曲线也顺带了解一下。
mathematica 演示二次贝塞尔曲线的计算
p0 = {1, 3}; p1 = {3, 5}; p2 = {6, 3};
PA1[t_] := p0 + (p1 - p0)*t; PA2[t_] := p1 + (p2 - p1)*t; PB1[t_] := (1 - t)PA1[t] + tPA2[t]; Animate[Show[ ParametricPlot[PA1[ta], {t, 0, 1}, PlotStyle -> {Thick, Blue}, PlotRange -> {{-1, 8}, {-1, 8}}, GridLines -> Automatic, GridLinesStyle -> LightGray, AxesLabel -> {"X", "Y"}], ParametricPlot[PA2[ta], {t, 0, 1}, PlotStyle -> {Thick, Blue}], Graphics[{Brown, Thick, Line[{PA1[a], PA2[a]}] (直接使用 a 作为参数)}], ParametricPlot[PB1[ta], {t, 0, 1}, PlotStyle -> {Thick, Red}], Graphics[{PointSize[0.01],(点的大小)Black,(点颜色) Point[{p0, p1, p2}],(绘制所有点)Black, Text[Style["p0", FontSize -> 12], p0, {0, 1.5}],({0, 1.5} 表示标签在点正上方*)Text[Style["p1", FontSize -> 12], p1, {0, 1.5}], Text[Style["p2", FontSize -> 12], p2, {0, 1.5}], GrayLevel[0.5],(直线颜色)Line[{p0, p1}],(连接 p0 和 p1) Line[{p1, p2}],}], ImageSize -> Large], {a, 0, 1, 0.01}, AnimationRunning -> False, RefreshRate -> 60]

有了矢量图形(SVG),便可以通过简单的方式 NanoSVG 将其显示出来。
NanoSVG 简介
NanoSVG 是一个简单高效且体积小巧的 SVG 解析库,仅以一个头文件(nanosvg.h)形式呈现。这款解析器可以直接处理 SVG 文件,将矢量图形转换为一系列基于三次贝塞尔曲线的路径数据,非常适合于比如图形显示、图标渲染以及游戏原型开发等场景。 在实际使用中,你只需要将 nanosvg.h 文件复制到你的项目中,并在一个 C 或 C++ 文件中定义 NANOSVG_IMPLEMENTATION 来展开实现。NanoSVG 能够辨识并转换 SVG 内部的路径、形状以及颜色信息,并支持多种单位(如 "px"、"pt"、"mm"、"in" 等),通过 DPI 的设置来转换实际像素或物理尺寸。如果你不关心单位转换问题,使用 "px" 和默认 DPI 96 通常就足够了。 此外,除了 SVG 解析功能,NanoSVG 还附带了一个简单的光栅化器,用于对解析后的 SVG 路径进行填充显示,尽管它的渲染速度和精度并非专业级别,但由于其轻量级的特性,非常适合在需要快速集成图形显示的项目中作为原型工具。 如果你希望获得更全面的 SVG 颜色支持,可以在实现前定义 NANOSVG_ALL_COLOR_KEYWORDS 来启用全列表的颜色关键字支持。总的来说,NanoSVG 是一个非常轻便且实用的工具,尤其适合用于嵌入式系统、编辑器界面自定义图标以及各类快速开发的场景中。2 关于更多用法或集成实例,你可能还会想探索如何在各类应用场景中优化 SVG 数据加载、内存管理以及与其他图形库的协作。也可以关注社区给出的实际案例和建议,在 GitHub 上查看更多源代码和实际应用示例。
示例代码:NanoSVG 解析 e.svg
#include <iostream>#include <math.h>#define NANOSVG_IMPLEMENTATION // 启用 NanoSVG 实现#include "nanosvg/src/nanosvg.h" #include <GLFW/glfw3.h> /** * 思路: * 1. 使用 nanosvg 解析 svg 文件 * 2. 绘制出对应的路径、锚点和控制点 */NSVGimage* g_image = NULL; static unsigned char bgColor[4] = {247,247,246,255};static unsigned char lineColor[4] = {0,0,255,255}; static float distPtSeg(float x, float y, float px, float py, float qx, float qy){ float pqx, pqy, dx, dy, d, t; pqx = qx-px; pqy = qy-py; dx = x-px; dy = y-py; d = pqx*pqx + pqy*pqy; t = pqx*dx + pqy*dy; if (d > 0) t /= d; if (t < 0) t = 0; else if (t > 1) t = 1; dx = px + t*pqx - x; dy = py + t*pqy - y; return dx*dx + dy*dy;} static void cubicBez(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tol, int level){ float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234; float d; if (level > 12) return; x12 = (x1+x2)*0.5f; y12 = (y1+y2)*0.5f; x23 = (x2+x3)*0.5f; y23 = (y2+y3)*0.5f; x34 = (x3+x4)*0.5f; y34 = (y3+y4)*0.5f; x123 = (x12+x23)*0.5f; y123 = (y12+y23)*0.5f; x234 = (x23+x34)*0.5f; y234 = (y23+y34)*0.5f; x1234 = (x123+x234)*0.5f; y1234 = (y123+y234)*0.5f; d = distPtSeg(x1234, y1234, x1,y1, x4,y4); if (d > tol*tol) { cubicBez(x1,y1, x12,y12, x123,y123, x1234,y1234, tol, level+1); cubicBez(x1234,y1234, x234,y234, x34,y34, x4,y4, tol, level+1); } else { glVertex2f(x4, y4); }} void drawPath(float* pts, int npts, char closed, float tol){ int i; glBegin(GL_LINE_STRIP); glColor4ubv(lineColor); glVertex2f(pts[0], pts[1]); for (i = 0; i < npts-1; i += 3) { float* p = &pts[i*2]; cubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7], tol, 0); } if (closed) { glVertex2f(pts[0], pts[1]); } glEnd();} void drawControlPts(float* pts, int npts){ int i; // Control lines glColor4ubv(lineColor); glBegin(GL_LINES); for (i = 0; i < npts-1; i += 3) { float* p = &pts[i*2]; glVertex2f(p[0],p[1]); glVertex2f(p[2],p[3]); glVertex2f(p[4],p[5]); glVertex2f(p[6],p[7]); } glEnd(); // Points glPointSize(6.0f); glColor4ubv(lineColor); glBegin(GL_POINTS); glVertex2f(pts[0],pts[1]); for (i = 0; i < npts-1; i += 3) { float* p = &pts[i*2]; glVertex2f(p[6],p[7]); } glEnd(); // Points glPointSize(3.0f); glBegin(GL_POINTS); glColor4ubv(bgColor); glVertex2f(pts[0],pts[1]); for (i = 0; i < npts-1; i += 3) { float* p = &pts[i*2]; glColor4ubv(lineColor); glVertex2f(p[2],p[3]); glVertex2f(p[4],p[5]); glColor4ubv(bgColor); glVertex2f(p[6],p[7]); } glEnd();} void drawframe(GLFWwindow* window){ int width = 0, height = 0; float view[4], cx, cy, hw, hh, aspect, px; NSVGshape* shape; NSVGpath* path; glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); glfwGetFramebufferSize(window, &width, &height); glViewport(0, 0, width, height); glClearColor(0.974f, 0.974f, 0.978f, 1.000f); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDisable(GL_TEXTURE_2D); glMatrixMode(GL_PROJECTION); glLoadIdentity(); // Fit view to bounds cx = g_image->width*0.5f; cy = g_image->height*0.5f; hw = g_image->width*0.5f; hh = g_image->height*0.5f; if (width/hw < height/hh) { aspect = (float)height / (float)width; view[0] = cx - hw * 1.2f; view[2] = cx + hw * 1.2f; view[1] = cy - hw * 1.2f * aspect; view[3] = cy + hw * 1.2f * aspect; } else { aspect = (float)width / (float)height; view[0] = cx - hh * 1.2f * aspect; view[2] = cx + hh * 1.2f * aspect; view[1] = cy - hh * 1.2f; view[3] = cy + hh * 1.2f; } // Size of one pixel. px = (view[2] - view[1]) / (float)width; glOrtho(view[0], view[2], view[3], view[1], -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glDisable(GL_DEPTH_TEST); glColor4ub(255,255,255,255); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); // Draw bounds glColor4ub(0,0,0,64); glBegin(GL_LINE_LOOP); glVertex2f(0, 0); glVertex2f(g_image->width, 0); glVertex2f(g_image->width, g_image->height); glVertex2f(0, g_image->height); glEnd(); for (shape = g_image->shapes; shape != NULL; shape = shape->next) { for (path = shape->paths; path != NULL; path = path->next) { drawPath(path->pts, path->npts, path->closed, px * 1.5f); drawControlPts(path->pts, path->npts); } } glfwSwapBuffers(window);} void resizecb(GLFWwindow* window, int width, int height){ // Update and render NSVG_NOTUSED(width); NSVG_NOTUSED(height); drawframe(window);} int main(){ GLFWwindow* window; const GLFWvidmode* mode; if (!glfwInit()) return -1; mode = glfwGetVideoMode(glfwGetPrimaryMonitor()); window = glfwCreateWindow(mode->width - 40, mode->height - 80, "Nano SVG", NULL, NULL); if (!window) { printf("Could not open window\n"); glfwTerminate(); return -1; } glfwSetFramebufferSizeCallback(window, resizecb); glfwMakeContextCurrent(window); glEnable(GL_POINT_SMOOTH); glEnable(GL_LINE_SMOOTH); g_image = nsvgParseFromFile("e.svg", "px", 96.0f); if (g_image == NULL) { printf("Could not open SVG image.\n"); glfwTerminate(); return -1; } while (!glfwWindowShouldClose(window)) { drawframe(window); glfwPollEvents(); } nsvgDelete(g_image); glfwTerminate(); return 0;} 
添加 stb_image_write.h,即可把 SVG 文件转换为图片。
💭 svg 转 png 很简单,反之要复杂很多。
示例代码:SVG 文件解析并转为 png 图片
#include <stdio.h>#include <string.h>#include <float.h>#define STB_IMAGE_WRITE_IMPLEMENTATION#include "stb_image_write.h"#define NANOSVG_IMPLEMENTATION#include "nanosvg.h"#define NANOSVGRAST_IMPLEMENTATION#include "nanosvgrast.h" /** * 1. 使用 nanosvg 解析 svg 文件 * 2. 导出为图片 */int main(){ NSVGimage *image = NULL; NSVGrasterizer *rast = NULL; unsigned char *img = NULL; int w, h; const char *filename = "e.svg"; printf("parsing %s\n", filename); image = nsvgParseFromFile(filename, "px", 48.0f); if (image == NULL) { printf("Could not open SVG image.\n"); goto error; } w = (int)image->width; h = (int)image->height; rast = nsvgCreateRasterizer(); if (rast == NULL) { printf("Could not init rasterizer.\n"); goto error; } img = (unsigned char *)malloc(w * h * 4); if (img == NULL) { printf("Could not alloc image buffer.\n"); goto error; } printf("rasterizing image %d x %d\n", w, h); nsvgRasterize(rast, image, 0, 0, 1, img, w, h, w * 4); printf("writing export_e.png\n"); stbi_write_png("export_e.png", w, h, 4, img, w * 4); error: nsvgDeleteRasterizer(rast); nsvgDelete(image); return 0;} 
手动解析 e.svg
基于上述示例,我们已了解 SVG 的基本概念。值得注意的是,NanoSVG 会将路径数据解析为包含三次贝塞尔曲线的结构(与 TrueType 使用的二次曲线不同,NanoSVG 通过插点把二次贝塞尔曲线转为三次贝塞尔曲线),因此我们可直接解析 e.svg 文件中的路径信息。
示例代码:解析 e.svg 中的点,并绘制这些点
#include <iostream>#include <math.h>#include <vector>#include <string>#include <cctype>#include <iomanip> #include <glad/glad.h>#include <GLFW/glfw3.h> /** * 思路: * 1. 解析 svg 文件中的点 * 2. 绘制这些点 */using namespace std; struct Point{ double x; double y;}; vector<string> tokenize(const string &path){ vector<string> tokens; string token; bool readingNumber = false; for (char c : path) { if (isdigit(c) || c == '.' || c == '-' || c == '+') { readingNumber = true; token += c; } else { if (readingNumber) { tokens.push_back(token); token.clear(); readingNumber = false; } if (isalpha(c) || c == ',') { if (c != ',') { // 忽略逗号 tokens.push_back(string(1, c)); } } } } if (!token.empty()) { tokens.push_back(token); } return tokens;} vector<Point> parseSVGPath(const string &pathData){ vector<Point> points; vector<string> tokens = tokenize(pathData); double currentX = 0.0; double currentY = 0.0; size_t i = 0; while (i < tokens.size()) { string cmd = tokens[i++]; if (cmd == "M" || cmd == "m") { // MoveTo double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "m") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "L" || cmd == "l") { // LineTo double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "l") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "Q" || cmd == "q") { // Quadratic Bezier // 跳过控制点(前两个参数) i += 2; double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "q") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "Z" || cmd == "z") { // ClosePath // 不需要处理坐标 } else { cerr << "Unsupported command: " << cmd << endl; break; } } return points;} // 顶点着色器const char *vertexShaderSource = R"( #version 330 core layout(location = 0) in vec2 aPos; void main() { gl_Position = vec4(aPos.x, -aPos.y, 0.0, 1.0); } )"; // 片段着色器const char *fragmentShaderSource = R"( #version 330 core out vec4 FragColor; void main() { FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色点 } )"; // 编译着色器GLuint compileShader(GLenum type, const char *source){ GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint success; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { char infoLog[512]; glGetShaderInfoLog(shader, 512, nullptr, infoLog); std::cerr << "ERROR: SHADER COMPILATION\n" << infoLog << std::endl; } return shader;} // 创建着色器程序GLuint createShaderProgram(){ GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource); GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); GLuint shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); return shaderProgram;} int main(){ string pathData = "M801.99 734.51 l26.55 26.55 q-97.35 112.83 -219.03 175.88 q-121.68 63.06 -238.93 63.06 q-130.54 0 -211.29 -84.07 q-80.75 -84.07 -80.75 -219.03 q0 -161.5 99.55 -325.22 q99.55 -163.72 255.53 -267.7 q155.98 -103.98 310.85 -103.98 q81.86 0 129.42 39.83 q47.57 39.83 47.57 110.61 q0 143.81 -159.29 256.64 q-159.29 112.83 -407.08 148.23 l-61.95 8.85 q-22.12 88.49 -22.12 137.17 q0 90.71 56.42 144.91 q56.42 54.2 151.55 54.2 q70.8 0 137.17 -34.29 q66.37 -34.29 185.84 -131.64 ZM308.63 515.49 q119.47 -28.76 209.07 -70.8 q89.6 -42.04 137.16 -81.86 q47.56 -39.82 76.33 -86.28 q28.77 -46.46 36.51 -77.44 q7.74 -30.98 7.74 -61.95 q0 -37.61 -21.02 -61.95 q-21.01 -24.34 -56.41 -24.34 q-84.07 0 -190.26 109.52 q-106.19 109.52 -161.5 257.75 l-37.61 97.35 Z"; vector<Point> tempPoints = parseSVGPath(pathData); cout << fixed << setprecision(2); cout << "Extracted Points (excluding Q/q control points):\n"; for (const auto &p : tempPoints) { cout << "(" << p.x << ", " << p.y << ")\n"; } if (!glfwInit()) { std::cerr << "无法初始化 GLFW" << std::endl; return -1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGL Render e.svg Points", nullptr, nullptr); if (!window) { std::cerr << "无法创建窗口" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cerr << "无法初始化 GLAD" << std::endl; glfwTerminate(); return -1; } GLuint shaderProgram = createShaderProgram(); std::vector<float> points; float offsetX = -0.5; float offsetY = -3.0; for (size_t i = 0; i < tempPoints.size(); ++i) { if (i == tempPoints.size() - 12) { continue; } points.push_back(tempPoints[i].x / 1000 + offsetX); points.push_back(tempPoints[i].y / 1000 + offsetX); } GLuint VAO, VBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, points.size() * sizeof(float), points.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0); glEnableVertexAttribArray(0); glPointSize(6.0f); while (!glfwWindowShouldClose(window)) { glClearColor(1.0f, 1.0f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_POINTS, 0, points.size() / 2); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteProgram(shaderProgram); glfwDestroyWindow(window); glfwTerminate(); return 0;}
💡 为了能看懂并修改 AI 工具生成的代码,使其运行通过,欢迎来到 OpenGL 的世界。
绘制这些点连接起来之后的线。
示例代码:解析 e.svg 文件并绘制直线
#include <iostream>#include <math.h>#include <vector>#include <string>#include <cctype>#include <iomanip> #include <glad/glad.h>#include <GLFW/glfw3.h> /** * 思路: * 1. 解析 svg 文件中的点 * 2. 相邻两点之间画线 * 问题: * svg path 中 m 标识跳过,两条闭合曲线 */using namespace std; struct Point{ double x; double y;}; vector<string> tokenize(const string &path){ vector<string> tokens; string token; bool readingNumber = false; for (char c : path) { if (isdigit(c) || c == '.' || c == '-' || c == '+') { readingNumber = true; token += c; } else { if (readingNumber) { tokens.push_back(token); token.clear(); readingNumber = false; } if (isalpha(c) || c == ',') { if (c != ',') { // 忽略逗号 tokens.push_back(string(1, c)); } } } } if (!token.empty()) { tokens.push_back(token); } return tokens;} vector<Point> parseSVGPath(const string &pathData){ vector<Point> points; vector<string> tokens = tokenize(pathData); double currentX = 0.0; double currentY = 0.0; size_t i = 0; while (i < tokens.size()) { string cmd = tokens[i++]; if (cmd == "M" || cmd == "m") { // MoveTo double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "m") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "L" || cmd == "l") { // LineTo double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "l") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "Q" || cmd == "q") { // Quadratic Bezier // 跳过控制点(前两个参数) i += 2; double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "q") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "Z" || cmd == "z") { // ClosePath // 不需要处理坐标 } else { cerr << "Unsupported command: " << cmd << endl; break; } } return points;} // 顶点着色器const char *vertexShaderSource = R"( #version 330 core layout(location = 0) in vec2 aPos; void main() { gl_Position = vec4(aPos.x, -aPos.y, 0.0, 1.0); } )"; // 片段着色器const char *fragmentShaderSource = R"( #version 330 core out vec4 FragColor; void main() { FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色点 } )"; // 编译着色器GLuint compileShader(GLenum type, const char *source){ GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint success; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { char infoLog[512]; glGetShaderInfoLog(shader, 512, nullptr, infoLog); std::cerr << "ERROR: SHADER COMPILATION\n" << infoLog << std::endl; } return shader;} // 创建着色器程序GLuint createShaderProgram(){ GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource); GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); GLuint shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); return shaderProgram;} int main(){ string pathData = "M801.99 734.51 l26.55 26.55 q-97.35 112.83 -219.03 175.88 q-121.68 63.06 -238.93 63.06 q-130.54 0 -211.29 -84.07 q-80.75 -84.07 -80.75 -219.03 q0 -161.5 99.55 -325.22 q99.55 -163.72 255.53 -267.7 q155.98 -103.98 310.85 -103.98 q81.86 0 129.42 39.83 q47.57 39.83 47.57 110.61 q0 143.81 -159.29 256.64 q-159.29 112.83 -407.08 148.23 l-61.95 8.85 q-22.12 88.49 -22.12 137.17 q0 90.71 56.42 144.91 q56.42 54.2 151.55 54.2 q70.8 0 137.17 -34.29 q66.37 -34.29 185.84 -131.64 ZM308.63 515.49 q119.47 -28.76 209.07 -70.8 q89.6 -42.04 137.16 -81.86 q47.56 -39.82 76.33 -86.28 q28.77 -46.46 36.51 -77.44 q7.74 -30.98 7.74 -61.95 q0 -37.61 -21.02 -61.95 q-21.01 -24.34 -56.41 -24.34 q-84.07 0 -190.26 109.52 q-106.19 109.52 -161.5 257.75 l-37.61 97.35 Z"; vector<Point> tempPoints = parseSVGPath(pathData); cout << fixed << setprecision(2); cout << "Extracted Points (excluding Q/q control points):\n"; for (const auto &p : tempPoints) { cout << "(" << p.x << ", " << p.y << ")\n"; } if (!glfwInit()) { std::cerr << "无法初始化 GLFW" << std::endl; return -1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGL Render e.svg Lines", nullptr, nullptr); if (!window) { std::cerr << "无法创建窗口" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cerr << "无法初始化 GLAD" << std::endl; glfwTerminate(); return -1; } GLuint shaderProgram = createShaderProgram(); std::vector<float> lines; float offsetX = -0.5; float offsetY = -3.0; for (size_t i = 0; i < tempPoints.size()-1; ++i) { // 这里减掉 svg path 中 zm 那条线 if (i == tempPoints.size() - 12) { continue; } lines.push_back(tempPoints[i].x / 1000 + offsetX); lines.push_back(tempPoints[i].y / 1000 + offsetX); lines.push_back(tempPoints[i+1].x / 1000 + offsetX); lines.push_back(tempPoints[i+1].y / 1000 + offsetX); } GLuint VAO, VBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(float), lines.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0); glEnableVertexAttribArray(0); glPointSize(4.0f); while (!glfwWindowShouldClose(window)) { glClearColor(0.974f, 0.974f, 0.978f, 1.000f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_LINES, 0, lines.size() / 2); glDrawArrays(GL_POINTS, 0, lines.size() / 2); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteProgram(shaderProgram); glfwDestroyWindow(window); glfwTerminate(); return 0;}
使用 Easy Scalable Text Rendering on the GPU 中的方式,选择一个参考点,与解析出的点(轮廓上的点)两两构成三角形。
示例代码:选择一个参考点,遍历解析出的点,每两个点与参考点组成一个三角形
#include <iostream>#include <math.h>#include <vector>#include <string>#include <sstream>#include <cctype>#include <iomanip> #include <glad/glad.h>#include <GLFW/glfw3.h> /** * 思路: * 1. 解析 svg 文件中的点 * 2. 选择一个参考点,遍历解析出的点,每两个点与参考点组成一个三角形 * 3. 绘制这些三角形 */using namespace std; struct Point{ double x; double y;}; vector<string> tokenize(const string &path){ vector<string> tokens; string token; bool readingNumber = false; for (char c : path) { if (isdigit(c) || c == '.' || c == '-' || c == '+') { readingNumber = true; token += c; } else { if (readingNumber) { tokens.push_back(token); token.clear(); readingNumber = false; } if (isalpha(c) || c == ',') { if (c != ',') { // 忽略逗号 tokens.push_back(string(1, c)); } } } } if (!token.empty()) { tokens.push_back(token); } return tokens;} vector<Point> parseSVGPath(const string &pathData){ vector<Point> points; vector<string> tokens = tokenize(pathData); double currentX = 0.0; double currentY = 0.0; size_t i = 0; while (i < tokens.size()) { string cmd = tokens[i++]; if (cmd == "M" || cmd == "m") { // MoveTo double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "m") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "L" || cmd == "l") { // LineTo double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "l") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "Q" || cmd == "q") { // Quadratic Bezier // 跳过控制点(前两个参数) i += 2; double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "q") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "Z" || cmd == "z") { // ClosePath // 不需要处理坐标 } else { cerr << "Unsupported command: " << cmd << endl; break; } } return points;} const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, -aPos.y, aPos.z, 1.0);\n" "}\0";const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(0.0f, 0.0f, 1.0f, 0.4f);\n" "}\n\0"; // 编译着色器GLuint compileShader(GLenum type, const char *source){ GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint success; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { char infoLog[512]; glGetShaderInfoLog(shader, 512, nullptr, infoLog); std::cerr << "ERROR: SHADER COMPILATION\n" << infoLog << std::endl; } return shader;} // 创建着色器程序GLuint createShaderProgram(){ GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource); GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); GLuint shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); return shaderProgram;} int main(){ string pathData = "M801.99 734.51 l26.55 26.55 q-97.35 112.83 -219.03 175.88 q-121.68 63.06 -238.93 63.06 q-130.54 0 -211.29 -84.07 q-80.75 -84.07 -80.75 -219.03 q0 -161.5 99.55 -325.22 q99.55 -163.72 255.53 -267.7 q155.98 -103.98 310.85 -103.98 q81.86 0 129.42 39.83 q47.57 39.83 47.57 110.61 q0 143.81 -159.29 256.64 q-159.29 112.83 -407.08 148.23 l-61.95 8.85 q-22.12 88.49 -22.12 137.17 q0 90.71 56.42 144.91 q56.42 54.2 151.55 54.2 q70.8 0 137.17 -34.29 q66.37 -34.29 185.84 -131.64 ZM308.63 515.49 q119.47 -28.76 209.07 -70.8 q89.6 -42.04 137.16 -81.86 q47.56 -39.82 76.33 -86.28 q28.77 -46.46 36.51 -77.44 q7.74 -30.98 7.74 -61.95 q0 -37.61 -21.02 -61.95 q-21.01 -24.34 -56.41 -24.34 q-84.07 0 -190.26 109.52 q-106.19 109.52 -161.5 257.75 l-37.61 97.35 Z"; vector<Point> tempPoints = parseSVGPath(pathData); cout << fixed << setprecision(2); cout << "Extracted Points (excluding Q/q control points):\n"; for (const auto &p : tempPoints) { cout << "(" << p.x << ", " << p.y << ")\n"; } // 初始化 GLFW if (!glfwInit()) { std::cerr << "无法初始化 GLFW" << std::endl; return -1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGL Render e.svg triangles", nullptr, nullptr); if (!window) { std::cerr << "无法创建窗口" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cerr << "无法初始化 GLAD" << std::endl; glfwTerminate(); return -1; } GLuint shaderProgram = createShaderProgram(); std::vector<float> tempVertices; float offsetX = -0.5f; float offsetY = -0.5f; // 添加三角形 for (size_t i = 0; i < tempPoints.size() - 1; i++) { if (i == tempPoints.size() - 12) { continue; } tempVertices.push_back(0.0 + offsetX); tempVertices.push_back(0.5 + offsetY); tempVertices.push_back(0.0); tempVertices.push_back(tempPoints[i].x / 1000 + offsetX); tempVertices.push_back(tempPoints[i].y / 1000 + offsetY); tempVertices.push_back(0.0); tempVertices.push_back(tempPoints[i + 1].x / 1000 + offsetX); tempVertices.push_back(tempPoints[i + 1].y / 1000 + offsetY); tempVertices.push_back(0.0); } float *vertices = new float[tempVertices.size()]; for (size_t i = 0; i < tempVertices.size(); ++i) { std::cout << "tempVertices[" << i << "] = " << tempVertices[i] << std::endl; vertices[i] = tempVertices[i]; } // 打印 数组长度 std::cout << "vertices size: " << tempVertices.size() << std::endl; unsigned int VBO, VAO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, tempVertices.size() * sizeof(float), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); while (!glfwWindowShouldClose(window)) { glClearColor(0.974f, 0.974f, 0.978f, 1.000f); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, tempVertices.size() / 3); // 6 indices starting at 0 -> 1 triangle glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0;}
使用边缘填充算法(类似),对三角形覆盖到的区域进行填充。与 Easy Scalable Text Rendering on the GPU 博客中提到的卷绕数实现有所不同,但原理是类似的,一个像素点被扫描过的次数是奇数还是偶数,奇数则上色,这种方式是不是只能用于单色的文字?
示例代码:填充轮廓区域
#include <iostream>#include <math.h>#include <vector>#include <string>#include <sstream>#include <cctype>#include <iomanip> #include <glad/glad.h>#include <GLFW/glfw3.h>#include <glm/glm.hpp> /** * 1. 解析 svg 文件中的点 * 2. 选择一个参考点,遍历解析的点,每两个与参考点组成一个三角形 * 3. 绘制对应的三角形 * 4. 边缘填充算法,奇数重叠的区域绘制,偶数重叠的区域不绘制 */using namespace std; struct Point{ double x; double y;}; vector<string> tokenize(const string &path){ vector<string> tokens; string token; bool readingNumber = false; for (char c : path) { if (isdigit(c) || c == '.' || c == '-' || c == '+') { readingNumber = true; token += c; } else { if (readingNumber) { tokens.push_back(token); token.clear(); readingNumber = false; } if (isalpha(c) || c == ',') { if (c != ',') { // 忽略逗号 tokens.push_back(string(1, c)); } } } } if (!token.empty()) { tokens.push_back(token); } return tokens;} vector<Point> parseSVGPath(const string &pathData){ vector<Point> points; vector<string> tokens = tokenize(pathData); double currentX = 0.0; double currentY = 0.0; size_t i = 0; while (i < tokens.size()) { string cmd = tokens[i++]; if (cmd == "M" || cmd == "m") { // MoveTo double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "m") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "L" || cmd == "l") { // LineTo double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "l") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "Q" || cmd == "q") { // Quadratic Bezier // 跳过控制点(前两个参数) i += 2; double x = stod(tokens[i++]); double y = stod(tokens[i++]); if (cmd == "q") { currentX += x; currentY += y; } else { currentX = x; currentY = y; } points.push_back({currentX, currentY}); } else if (cmd == "Z" || cmd == "z") { // ClosePath // 不需要处理坐标 } else { cerr << "Unsupported command: " << cmd << endl; break; } } return points;} const char *accumVertexShaderSource = R"(#version 330 corelayout (location = 0) in vec2 aPos;void main() { gl_Position = vec4(aPos, 0.0, 1.0);})"; const char *accumFragmentShaderSource = R"(#version 330 coreout vec4 FragColor;void main() { FragColor = vec4(1.0/255.0, 0.0, 0.0, 1.0);})"; const char *displayVertexShaderSource = R"(#version 330 corelayout (location = 0) in vec2 aPos;out vec2 TexCoord;void main() { gl_Position = vec4(aPos, 0.0, 1.0); TexCoord = aPos * 0.5 + 0.5;})"; const char *displayFragmentShaderSource = R"(#version 330 corein vec2 TexCoord;out vec4 FragColor;uniform sampler2D accumTexture;void main() { float value = texture(accumTexture, TexCoord).r; int count = int(round(value * 255.0)); FragColor = (count % 2 == 1) ? vec4(0.0f, 0.0f, 1.0f, 1.0f) : vec4(0.974f, 0.974f, 0.978f, 1.000f);})"; GLuint compileShader(GLenum type, const char *source){ GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint success; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { char infoLog[512]; glGetShaderInfoLog(shader, 512, nullptr, infoLog); std::cerr << "Shader Error:\n" << infoLog << std::endl; } return shader;} GLuint createProgram(const char *vertSrc, const char *fragSrc){ GLuint vert = compileShader(GL_VERTEX_SHADER, vertSrc); GLuint frag = compileShader(GL_FRAGMENT_SHADER, fragSrc); GLuint program = glCreateProgram(); glAttachShader(program, vert); glAttachShader(program, frag); glLinkProgram(program); GLint success; glGetProgramiv(program, GL_LINK_STATUS, &success); if (!success) { char infoLog[512]; glGetProgramInfoLog(program, 512, nullptr, infoLog); std::cerr << "Program Error:\n" << infoLog << std::endl; } glDeleteShader(vert); glDeleteShader(frag); return program;} int main(){ // 初始化GLFW if (!glfwInit()) { std::cerr << "Failed to initialize GLFW" << std::endl; return -1; } // 配置GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 创建窗口 GLFWwindow *window = glfwCreateWindow(800, 600, "e.svg fill", nullptr, nullptr); if (!window) { std::cerr << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } // 设置上下文 glfwMakeContextCurrent(window); // 初始化GLAD if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cerr << "Failed to initialize GLAD" << std::endl; return -1; } // 设置视口 glViewport(0, 0, 800, 600); // 创建离屏FBO GLuint fbo; glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); // 创建累积纹理 GLuint accumTexture; glGenTextures(1, &accumTexture); glBindTexture(GL_TEXTURE_2D, accumTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 800, 600, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, accumTexture, 0); // 检查FBO完整性 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { std::cerr << "Framebuffer is not complete!" << std::endl; } // 创建着色器程序 GLuint accumProgram = createProgram(accumVertexShaderSource, accumFragmentShaderSource); GLuint displayProgram = createProgram(displayVertexShaderSource, displayFragmentShaderSource); std::vector<glm::vec2> vertices; string pathData = "M801.99 734.51 l26.55 26.55 q-97.35 112.83 -219.03 175.88 q-121.68 63.06 -238.93 63.06 q-130.54 0 -211.29 -84.07 q-80.75 -84.07 -80.75 -219.03 q0 -161.5 99.55 -325.22 q99.55 -163.72 255.53 -267.7 q155.98 -103.98 310.85 -103.98 q81.86 0 129.42 39.83 q47.57 39.83 47.57 110.61 q0 143.81 -159.29 256.64 q-159.29 112.83 -407.08 148.23 l-61.95 8.85 q-22.12 88.49 -22.12 137.17 q0 90.71 56.42 144.91 q56.42 54.2 151.55 54.2 q70.8 0 137.17 -34.29 q66.37 -34.29 185.84 -131.64 ZM308.63 515.49 q119.47 -28.76 209.07 -70.8 q89.6 -42.04 137.16 -81.86 q47.56 -39.82 76.33 -86.28 q28.77 -46.46 36.51 -77.44 q7.74 -30.98 7.74 -61.95 q0 -37.61 -21.02 -61.95 q-21.01 -24.34 -56.41 -24.34 q-84.07 0 -190.26 109.52 q-106.19 109.52 -161.5 257.75 l-37.61 97.35 Z"; vector<Point> tempPoints = parseSVGPath(pathData); float offsetX = -0.5f; float offsetY = -0.5f; // 添加三角形 for (size_t i = 0; i < tempPoints.size() - 1; i++) { if (i == tempPoints.size() - 12) { continue; } vertices.push_back({-1.0 + offsetX, -(0.5 + offsetY)}); vertices.push_back({tempPoints[i].x / 1000 + offsetX, -(tempPoints[i].y / 1000 + offsetY)}); vertices.push_back({tempPoints[i + 1].x / 1000 + offsetX, -(tempPoints[i + 1].y / 1000 + offsetY)}); } // 设置VAO/VBO GLuint vao, vbo; glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(glm::vec2), vertices.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *)0); glEnableVertexAttribArray(0); // 全屏四边形数据 float quadVertices[] = { -1.0f, 1.0f, // 左上 -1.0f, -1.0f, // 左下 1.0f, -1.0f, // 右下 -1.0f, 1.0f, // 左上 1.0f, -1.0f, // 右下 1.0f, 1.0f // 右上 }; // 设置全屏四边形VAO/VBO GLuint quadVAO, quadVBO; glGenVertexArrays(1, &quadVAO); glGenBuffers(1, &quadVBO); glBindVertexArray(quadVAO); glBindBuffer(GL_ARRAY_BUFFER, quadVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *)0); glEnableVertexAttribArray(0); // 主循环 while (!glfwWindowShouldClose(window)) { // 处理输入 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); // 第一步:累积绕数 glBindFramebuffer(GL_FRAMEBUFFER, fbo); glClearColor(0.974f, 0.974f, 0.978f, 1.000f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(accumProgram); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE); glBindVertexArray(vao); glDrawArrays(GL_TRIANGLES, 0, vertices.size()); glDisable(GL_BLEND); // 第二步:显示结果 glBindFramebuffer(GL_FRAMEBUFFER, 0); glClearColor(0.0f, 0.0f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(displayProgram); glBindTexture(GL_TEXTURE_2D, accumTexture); glUniform1i(glGetUniformLocation(displayProgram, "accumTexture"), 0); glBindVertexArray(quadVAO); glDrawArrays(GL_TRIANGLES, 0, 6); // 交换缓冲区和轮询事件 glfwSwapBuffers(window); glfwPollEvents(); } // 清理资源 glDeleteVertexArrays(1, &vao); glDeleteBuffers(1, &vbo); glDeleteVertexArrays(1, &quadVAO); glDeleteBuffers(1, &quadVBO); glDeleteTextures(1, &accumTexture); glDeleteFramebuffers(1, &fbo); glDeleteProgram(accumProgram); glDeleteProgram(displayProgram); glfwTerminate(); return 0;}
在解析点的过程中,对二次贝塞尔曲线进行插值,使其轮廓更加细密。
示例代码:填充轮廓区域
#include <iostream>#include <math.h>#include <vector>#include <string>#include <sstream>#include <cctype>#include <iomanip> #include <glad/glad.h>#include <GLFW/glfw3.h>#include <glm/glm.hpp> /** * 1. 解析 svg 文件中的点,对二次贝塞尔曲线进行插值 * 2. 选择一个参考点,遍历解析的点,每两个与参考点组成一个三角形 * 3. 绘制对应的三角形 * 4. 边缘填充算法,奇数重叠的区域绘制,偶数重叠的区域不绘制 */using namespace std; // 2D 点结构struct Point{ float x, y;}; // 一条子路径:按照顺序排列的点using PathSegment = std::vector<Point>; std::string normalizePathString(const std::string &path){ std::string result; for (size_t i = 0; i < path.size(); i++) { char c = path[i]; if (std::isalpha(c)) { result.push_back(' '); result.push_back(c); result.push_back(' '); } else { result.push_back(c); } } return result;} std::vector<PathSegment> parseSVGPath(const std::string &pathDataIn){ std::string pathData = normalizePathString(pathDataIn); std::vector<PathSegment> segments; PathSegment currentSegment; Point current = {0.0f, 0.0f}; Point startPoint = {0.0f, 0.0f}; std::istringstream stream(pathData); std::string token; while (stream >> token) { char cmd = token[0]; switch (cmd) { case 'M': case 'm': { float x, y; stream >> x >> y; Point p; if (cmd == 'm') { p.x = current.x + x; p.y = current.y + y; } else { p.x = x; p.y = y; } if (!currentSegment.empty()) { segments.push_back(currentSegment); currentSegment.clear(); } current = p; startPoint = p; currentSegment.push_back(p); break; } case 'L': case 'l': { float x, y; stream >> x >> y; Point p; if (cmd == 'l') { p.x = current.x + x; p.y = current.y + y; } else { p.x = x; p.y = y; } current = p; currentSegment.push_back(p); break; } case 'Q': case 'q': { float cx, cy, ex, ey; stream >> cx >> cy >> ex >> ey; Point control, end; if (cmd == 'q') { control.x = current.x + cx; control.y = current.y + cy; end.x = current.x + ex; end.y = current.y + ey; } else { control.x = cx; control.y = cy; end.x = ex; end.y = ey; } const int samples = 20; for (int i = 1; i <= samples; i++) { float t = float(i) / samples; float inv = 1.0f - t; float x = inv * inv * current.x + 2 * inv * t * control.x + t * t * end.x; float y = inv * inv * current.y + 2 * inv * t * control.y + t * t * end.y; currentSegment.push_back({x, y}); } current = end; break; } case 'Z': case 'z': { // 闭合路径:将起点加入末尾 currentSegment.push_back(startPoint); segments.push_back(currentSegment); currentSegment.clear(); current = startPoint; break; } default: std::cerr << "不支持的命令: " << cmd << std::endl; break; } } if (!currentSegment.empty()) segments.push_back(currentSegment); return segments;} const char *accumVertexShaderSource = R"(#version 330 corelayout (location = 0) in vec2 aPos;void main() { gl_Position = vec4(aPos, 0.0, 1.0);})"; const char *accumFragmentShaderSource = R"(#version 330 coreout vec4 FragColor;void main() { FragColor = vec4(1.0/255.0, 0.0, 0.0, 1.0);})"; const char *displayVertexShaderSource = R"(#version 330 corelayout (location = 0) in vec2 aPos;out vec2 TexCoord;void main() { gl_Position = vec4(aPos, 0.0, 1.0); TexCoord = aPos * 0.5 + 0.5;})"; const char *displayFragmentShaderSource = R"(#version 330 corein vec2 TexCoord;out vec4 FragColor;uniform sampler2D accumTexture;void main() { float value = texture(accumTexture, TexCoord).r; int count = int(round(value * 255.0)); FragColor = (count % 2 == 1) ? vec4(0.0f, 0.0f, 1.0f, 1.0f) : vec4(0.974f, 0.974f, 0.978f, 1.000f);})"; GLuint compileShader(GLenum type, const char *source){ GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint success; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { char infoLog[512]; glGetShaderInfoLog(shader, 512, nullptr, infoLog); std::cerr << "Shader Error:\n" << infoLog << std::endl; } return shader;} GLuint createProgram(const char *vertSrc, const char *fragSrc){ GLuint vert = compileShader(GL_VERTEX_SHADER, vertSrc); GLuint frag = compileShader(GL_FRAGMENT_SHADER, fragSrc); GLuint program = glCreateProgram(); glAttachShader(program, vert); glAttachShader(program, frag); glLinkProgram(program); GLint success; glGetProgramiv(program, GL_LINK_STATUS, &success); if (!success) { char infoLog[512]; glGetProgramInfoLog(program, 512, nullptr, infoLog); std::cerr << "Program Error:\n" << infoLog << std::endl; } glDeleteShader(vert); glDeleteShader(frag); return program;} int main(){ // 初始化GLFW if (!glfwInit()) { std::cerr << "Failed to initialize GLFW" << std::endl; return -1; } // 配置GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 创建窗口 GLFWwindow *window = glfwCreateWindow(800, 600, "e.svg conic fill", nullptr, nullptr); if (!window) { std::cerr << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } // 设置上下文 glfwMakeContextCurrent(window); // 初始化GLAD if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cerr << "Failed to initialize GLAD" << std::endl; return -1; } // 设置视口 glViewport(0, 0, 800, 600); // 创建离屏FBO GLuint fbo; glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); // 创建累积纹理 GLuint accumTexture; glGenTextures(1, &accumTexture); glBindTexture(GL_TEXTURE_2D, accumTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 800, 600, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, accumTexture, 0); // 检查FBO完整性 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { std::cerr << "Framebuffer is not complete!" << std::endl; } // 创建着色器程序 GLuint accumProgram = createProgram(accumVertexShaderSource, accumFragmentShaderSource); GLuint displayProgram = createProgram(displayVertexShaderSource, displayFragmentShaderSource); std::vector<glm::vec2> vertices; string pathData = "M801.99 734.51 l26.55 26.55 q-97.35 112.83 -219.03 175.88 q-121.68 63.06 -238.93 63.06 q-130.54 0 -211.29 -84.07 q-80.75 -84.07 -80.75 -219.03 q0 -161.5 99.55 -325.22 q99.55 -163.72 255.53 -267.7 q155.98 -103.98 310.85 -103.98 q81.86 0 129.42 39.83 q47.57 39.83 47.57 110.61 q0 143.81 -159.29 256.64 q-159.29 112.83 -407.08 148.23 l-61.95 8.85 q-22.12 88.49 -22.12 137.17 q0 90.71 56.42 144.91 q56.42 54.2 151.55 54.2 q70.8 0 137.17 -34.29 q66.37 -34.29 185.84 -131.64 ZM308.63 515.49 q119.47 -28.76 209.07 -70.8 q89.6 -42.04 137.16 -81.86 q47.56 -39.82 76.33 -86.28 q28.77 -46.46 36.51 -77.44 q7.74 -30.98 7.74 -61.95 q0 -37.61 -21.02 -61.95 q-21.01 -24.34 -56.41 -24.34 q-84.07 0 -190.26 109.52 q-106.19 109.52 -161.5 257.75 l-37.61 97.35 Z"; std::vector<PathSegment> segments = parseSVGPath(pathData); if (segments.empty()) { std::cerr << "未解析出路径数据!" << std::endl; return -1; } float offsetX = -0.5f; float offsetY = -0.5f; // 添加三角形 for (const auto &segment : segments) { for (size_t i = 0; i < segment.size() - 1; i++) { vertices.push_back({-1.0 + offsetX, -(0.5 + offsetY)}); vertices.push_back({segment[i].x / 1000 + offsetX, -(segment[i].y / 1000 + offsetY)}); vertices.push_back({segment[i + 1].x / 1000 + offsetX, -(segment[i + 1].y / 1000 + offsetY)}); } } // 设置VAO/VBO GLuint vao, vbo; glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(glm::vec2), vertices.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *)0); glEnableVertexAttribArray(0); // 全屏四边形数据 float quadVertices[] = { -1.0f, 1.0f, // 左上 -1.0f, -1.0f, // 左下 1.0f, -1.0f, // 右下 -1.0f, 1.0f, // 左上 1.0f, -1.0f, // 右下 1.0f, 1.0f // 右上 }; // 设置全屏四边形VAO/VBO GLuint quadVAO, quadVBO; glGenVertexArrays(1, &quadVAO); glGenBuffers(1, &quadVBO); glBindVertexArray(quadVAO); glBindBuffer(GL_ARRAY_BUFFER, quadVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *)0); glEnableVertexAttribArray(0); // 主循环 while (!glfwWindowShouldClose(window)) { // 处理输入 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); // 第一步:累积绕数 glBindFramebuffer(GL_FRAMEBUFFER, fbo); glClearColor(0.974f, 0.974f, 0.978f, 1.000f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(accumProgram); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE); glBindVertexArray(vao); glDrawArrays(GL_TRIANGLES, 0, vertices.size()); glDisable(GL_BLEND); // 第二步:显示结果 glBindFramebuffer(GL_FRAMEBUFFER, 0); glClearColor(0.0f, 0.0f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(displayProgram); glBindTexture(GL_TEXTURE_2D, accumTexture); glUniform1i(glGetUniformLocation(displayProgram, "accumTexture"), 0); glBindVertexArray(quadVAO); glDrawArrays(GL_TRIANGLES, 0, 6); // 交换缓冲区和轮询事件 glfwSwapBuffers(window); glfwPollEvents(); } // 清理资源 glDeleteVertexArrays(1, &vao); glDeleteBuffers(1, &vbo); glDeleteVertexArrays(1, &quadVAO); glDeleteBuffers(1, &quadVBO); glDeleteTextures(1, &accumTexture); glDeleteFramebuffers(1, &fbo); glDeleteProgram(accumProgram); glDeleteProgram(displayProgram); glfwTerminate(); return 0;}
轮廓区域填充本质上是对多边形进行填充,其核心挑战在于如何将多边形高效三角剖分为多个三角形(供 OpenGL 光栅化处理)。除前述方法外,还存在多种三角剖分算法,例如耳切法(Ear Clipping)。
示例代码:使用耳切算法
#include <iostream>#include <math.h>#include <vector>#include <string>#include <sstream>#include <cctype>#include <cstdlib>#include <limits>#include <utility>#include <cmath>#include <algorithm> #include <glad/glad.h>#include <GLFW/glfw3.h> /** * 总结: * 1. 解析 SVG 路径数据,支持 M/m、L/l、Q/q 和 Z/z 命令。 * 2. 使用耳切算法对闭合简单多边形进行三角剖分,返回顶点数据。 * 3. 计算多边形面积,用于判断外部轮廓和内孔。 * 4. 计算正交变换矩阵,将所有子路径归一化到 [-1,1] 范围。 * 5. 使用 OpenGL 渲染外部轮廓和内孔,外部轮廓填充为黄色,内孔用背景色覆盖实现镂空效果。 * 6. 绘制所有轮廓线条,外部轮廓为黄色,内孔为背景色,轮廓线为白色。 */ // 2D 点结构struct Point { float x, y;}; // 一条子路径:按照顺序排列的点using PathSegment = std::vector<Point>; const float EPSILON = 0.001f; // ---------------------- SVG Path 解析器 ----------------------// 支持 M/m、L/l、Q/q 和 Z/z 命令。为了方便解析,// 先在命令字母前后加入空格,使得可以按 token 逐个读取。 std::string normalizePathString(const std::string& path) { std::string result; for (size_t i = 0; i < path.size(); i++) { char c = path[i]; if (std::isalpha(c)) { result.push_back(' '); result.push_back(c); result.push_back(' '); } else { result.push_back(c); } } return result;} std::vector<PathSegment> parseSVGPath(const std::string& pathDataIn) { std::string pathData = normalizePathString(pathDataIn); std::vector<PathSegment> segments; PathSegment currentSegment; Point current = {0.0f, 0.0f}; Point startPoint = {0.0f, 0.0f}; std::istringstream stream(pathData); std::string token; while (stream >> token) { char cmd = token[0]; switch (cmd) { case 'M': case 'm': { float x, y; stream >> x >> y; Point p; if (cmd == 'm') { p.x = current.x + x; p.y = current.y + y; } else { p.x = x; p.y = y; } if (!currentSegment.empty()) { segments.push_back(currentSegment); currentSegment.clear(); } current = p; startPoint = p; currentSegment.push_back(p); break; } case 'L': case 'l': { float x, y; stream >> x >> y; Point p; if (cmd == 'l') { p.x = current.x + x; p.y = current.y + y; } else { p.x = x; p.y = y; } current = p; currentSegment.push_back(p); break; } case 'Q': case 'q': { float cx, cy, ex, ey; stream >> cx >> cy >> ex >> ey; Point control, end; if (cmd == 'q') { control.x = current.x + cx; control.y = current.y + cy; end.x = current.x + ex; end.y = current.y + ey; } else { control.x = cx; control.y = cy; end.x = ex; end.y = ey; } const int samples = 20; for (int i = 1; i <= samples; i++) { float t = float(i) / samples; float inv = 1.0f - t; float x = inv * inv * current.x + 2 * inv * t * control.x + t * t * end.x; float y = inv * inv * current.y + 2 * inv * t * control.y + t * t * end.y; currentSegment.push_back({x, y}); } current = end; break; } case 'Z': case 'z': { // 闭合路径:将起点加入末尾 currentSegment.push_back(startPoint); segments.push_back(currentSegment); currentSegment.clear(); current = startPoint; break; } default: std::cerr << "不支持的命令: " << cmd << std::endl; break; } } if (!currentSegment.empty()) segments.push_back(currentSegment); return segments;} // ---------------------- 耳切算法三角剖分 ----------------------// 对闭合简单多边形进行三角剖分,返回排列为一维数组,每6个 float 表示一个三角形的三个顶点(x,y)。std::vector<float> triangulatePolygon(const PathSegment& polyInput) { std::vector<float> result; if(polyInput.size() < 3) return result; // 复制顶点;如果首尾重复则去除末尾重复的点 std::vector<Point> vertices = polyInput; if(std::fabs(vertices.front().x - vertices.back().x) < EPSILON && std::fabs(vertices.front().y - vertices.back().y) < EPSILON) { vertices.pop_back(); } int n = vertices.size(); if(n < 3) return result; // 判断顶点排列是否为逆时针 float area = 0.0f; for (int i = 0; i < n; i++){ int j = (i+1) % n; area += vertices[i].x * vertices[j].y - vertices[j].x * vertices[i].y; } if(area < 0) std::reverse(vertices.begin(), vertices.end()); // 用 indices 记录当前多边形 std::vector<int> indices(n); for (int i=0; i<n; i++) { indices[i] = i; } auto isConvex = [&](int i0, int i1, int i2) -> bool { Point a = vertices[i0], b = vertices[i1], c = vertices[i2]; float cross = (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x); return cross > 0; }; auto pointInTriangle = [&](const Point &p, const Point &a, const Point &b, const Point &c) -> bool { auto sign = [](const Point &p1, const Point &p2, const Point &p3) { return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); }; float d1 = sign(p, a, b); float d2 = sign(p, b, c); float d3 = sign(p, c, a); bool hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0); bool hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0); return !(hasNeg && hasPos); }; while(indices.size() > 2) { bool earFound = false; for (size_t i = 0; i < indices.size(); i++) { int prev = indices[(i + indices.size() - 1) % indices.size()]; int curr = indices[i]; int next = indices[(i + 1) % indices.size()]; if (!isConvex(prev, curr, next)) continue; bool hasPointInside = false; for (size_t j = 0; j < indices.size(); j++) { if (j == ((i + indices.size() - 1) % indices.size()) || j == i || j == ((i + 1) % indices.size())) continue; int idx = indices[j]; if(pointInTriangle(vertices[idx], vertices[prev], vertices[curr], vertices[next])) { hasPointInside = true; break; } } if(hasPointInside) continue; // 找到一个耳,记录三角形 result.push_back(vertices[prev].x); result.push_back(vertices[prev].y); result.push_back(vertices[curr].x); result.push_back(vertices[curr].y); result.push_back(vertices[next].x); result.push_back(vertices[next].y); indices.erase(indices.begin() + i); earFound = true; break; } if(!earFound) break; } return result;} // ---------------------- 计算多边形面积 ----------------------// 用于判断哪个子路径作为外部轮廓、哪些作为内孔。float polygonArea(const PathSegment &seg) { float area = 0.0f; // 若首尾重复则只计算一次 int n = seg.size(); if(n < 3) return 0.0f; if (std::fabs(seg.front().x - seg.back().x) < EPSILON && std::fabs(seg.front().y - seg.back().y) < EPSILON) n--; for (int i = 0; i < n; i++){ int j = (i + 1) % n; area += seg[i].x * seg[j].y - seg[j].x * seg[i].y; } return std::fabs(area) / 2.0f;} // ---------------------- 正交变换矩阵 ----------------------// 计算所有子路径的边界,然后构造一个将图形归一化到 [-1,1] 的 4x4 变换矩阵。// 此处不处理 Y 翻转,翻转在顶点着色器中处理。void computeTransformMatrix(const std::vector<PathSegment>& segments, float matrix[16]) { float xmin = std::numeric_limits<float>::max(); float ymin = std::numeric_limits<float>::max(); float xmax = std::numeric_limits<float>::lowest(); float ymax = std::numeric_limits<float>::lowest(); for (const auto& seg : segments) { for (const auto& p : seg) { if(p.x < xmin) xmin = p.x; if(p.x > xmax) xmax = p.x; if(p.y < ymin) ymin = p.y; if(p.y > ymax) ymax = p.y; } } float scaleX = 1.0f / (xmax - xmin); float scaleY = 1.0f / (ymax - ymin); float scale = (scaleX < scaleY) ? scaleX : scaleY; float tx = -(xmax + xmin) / 2.0f; float ty = -(ymax + ymin) / 2.0f; for (int i = 0; i < 16; i++) matrix[i] = 0.0f; matrix[0] = scale; matrix[5] = scale; matrix[10] = 1.0f; matrix[12] = scale * tx; matrix[13] = scale * ty; matrix[15] = 1.0f;} // ---------------------- OpenGL 着色器 ----------------------// 顶点着色器:乘以变换矩阵后再对 Y 坐标取反,实现垂直翻转。const char* vertexShaderSource = R"(#version 330 corelayout (location = 0) in vec2 aPos;uniform mat4 uTransform;void main() { vec4 pos = uTransform * vec4(aPos, 0.0, 1.0); pos.y = -pos.y; // Y 轴翻转 gl_Position = pos;})"; // 片元着色器:输出 uniform 指定的颜色。const char* fragmentShaderSource = R"(#version 330 coreuniform vec4 uColor;out vec4 FragColor;void main(){ FragColor = uColor;})"; GLuint createShaderProgram() { auto compileShader = [&](GLenum type, const char* source) -> GLuint { GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); int success; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { char infoLog[512]; glGetShaderInfoLog(shader, 512, nullptr, infoLog); std::cerr << "Shader Compile Error:\n" << infoLog << std::endl; } return shader; }; GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSource); GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); GLuint prog = glCreateProgram(); glAttachShader(prog, vs); glAttachShader(prog, fs); glLinkProgram(prog); int success; glGetProgramiv(prog, GL_LINK_STATUS, &success); if (!success) { char infoLog[512]; glGetProgramInfoLog(prog, 512, nullptr, infoLog); std::cerr << "Shader Link Error:\n" << infoLog << std::endl; } glDeleteShader(vs); glDeleteShader(fs); return prog;} // ---------------------- 主程序 ----------------------int main() { // SVG Path 数据,注意此路径(例如代表字母 "e")内部包含两个闭合子路径,外部轮廓和内孔 std::string pathData = "M801.99 734.51 l26.55 26.55 q-97.35 112.83 -219.03 175.88 q-121.68 63.06 -238.93 63.06 " "q-130.54 0 -211.29 -84.07 q-80.75 -84.07 -80.75 -219.03 q0 -161.5 99.55 -325.22 " "q99.55 -163.72 255.53 -267.7 q155.98 -103.98 310.85 -103.98 q81.86 0 129.42 39.83 " "q47.57 39.83 47.57 110.61 q0 143.81 -159.29 256.64 q-159.29 112.83 -407.08 148.23 " "l-61.95 8.85 q-22.12 88.49 -22.12 137.17 q0 90.71 56.42 144.91 q56.42 54.2 151.55 54.2 " "q70.8 0 137.17 -34.29 q66.37 -34.29 185.84 -131.64 ZM308.63 515.49 q119.47 -28.76 209.07 -70.8 " "q89.6 -42.04 137.16 -81.86 q47.56 -39.82 76.33 -86.28 q28.77 -46.46 36.51 -77.44 " "q7.74 -30.98 7.74 -61.95 q0 -37.61 -21.02 -61.95 q-21.01 -24.34 -56.41 -24.34 " "q-84.07 0 -190.26 109.52 q-106.19 109.52 -161.5 257.75 l-37.61 97.35 Z"; // 解析 SVG 路径数据 std::vector<PathSegment> segments = parseSVGPath(pathData); if(segments.empty()){ std::cerr << "未解析出路径数据!" << std::endl; return -1; } // 分离外部轮廓与内孔: // 这里我们认为面积最大的子路径为外部轮廓,其它视为内孔(镂空区域)。 float maxArea = 0.0f; int outerIndex = 0; for (size_t i = 0; i < segments.size(); i++){ float a = polygonArea(segments[i]); if(a > maxArea) { maxArea = a; outerIndex = i; } } std::vector<PathSegment> outerSegments, holeSegments; for (size_t i = 0; i < segments.size(); i++){ if ((int)i == outerIndex) outerSegments.push_back(segments[i]); else holeSegments.push_back(segments[i]); } // 分别对外部轮廓和内孔使用耳切算法进行三角剖分 std::vector<float> outerFillVertices; for (const auto& seg : outerSegments) { std::vector<float> tris = triangulatePolygon(seg); if(!tris.empty()) outerFillVertices.insert(outerFillVertices.end(), tris.begin(), tris.end()); } std::vector<float> holeFillVertices; for (const auto& seg : holeSegments) { std::vector<float> tris = triangulatePolygon(seg); if(!tris.empty()) holeFillVertices.insert(holeFillVertices.end(), tris.begin(), tris.end()); } // 构建轮廓顶点数据(用于绘制外部轮廓线) std::vector<float> outlineVertices; std::vector<std::pair<int,int>> drawRanges; for (const auto& seg : segments) { int startIndex = outlineVertices.size() / 2; for (const auto& p : seg) { outlineVertices.push_back(p.x); outlineVertices.push_back(p.y); } int count = (outlineVertices.size() / 2) - startIndex; drawRanges.push_back({startIndex, count}); } // 初始化 GLFW 与 OpenGL 上下文 if(!glfwInit()){ std::cerr << "无法初始化 GLFW" << std::endl; return -1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow* window = glfwCreateWindow(800, 600, "SVG Renderer - Hollow 'e'", nullptr, nullptr); if(!window){ std::cerr << "无法创建窗口" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){ std::cerr << "无法初始化 GLAD" << std::endl; return -1; } GLuint shaderProgram = createShaderProgram(); // 创建外部轮廓(填充用)VAO/VBO GLuint outerVAO, outerVBO; glGenVertexArrays(1, &outerVAO); glGenBuffers(1, &outerVBO); glBindVertexArray(outerVAO); glBindBuffer(GL_ARRAY_BUFFER, outerVBO); glBufferData(GL_ARRAY_BUFFER, outerFillVertices.size() * sizeof(float), outerFillVertices.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 创建内孔(填充用)VAO/VBO GLuint holeVAO = 0, holeVBO = 0; if(!holeFillVertices.empty()){ glGenVertexArrays(1, &holeVAO); glGenBuffers(1, &holeVBO); glBindVertexArray(holeVAO); glBindBuffer(GL_ARRAY_BUFFER, holeVBO); glBufferData(GL_ARRAY_BUFFER, holeFillVertices.size() * sizeof(float), holeFillVertices.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); glEnableVertexAttribArray(0); } // 创建轮廓线 VAO/VBO GLuint outlineVAO, outlineVBO; glGenVertexArrays(1, &outlineVAO); glGenBuffers(1, &outlineVBO); glBindVertexArray(outlineVAO); glBindBuffer(GL_ARRAY_BUFFER, outlineVBO); glBufferData(GL_ARRAY_BUFFER, outlineVertices.size() * sizeof(float), outlineVertices.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 计算变换矩阵,将图形归一化到 [-1,1],变换不处理 Y 翻转(在着色器中实现)。 float transform[16]; computeTransformMatrix(segments, transform); glClearColor(0.974f, 0.974f, 0.978f, 1.000f); glLineWidth(4.0f); GLint transformLoc = glGetUniformLocation(shaderProgram, "uTransform"); GLint colorLoc = glGetUniformLocation(shaderProgram, "uColor"); while(!glfwWindowShouldClose(window)){ glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glUniformMatrix4fv(transformLoc, 1, GL_FALSE, transform); // 渲染外部轮廓填充(蓝色) if(!outerFillVertices.empty()){ glUniform4f(colorLoc, 0.0f, 0.0f, 1.0f, 1.0f); glBindVertexArray(outerVAO); glDrawArrays(GL_TRIANGLES, 0, outerFillVertices.size()/2); } // 渲染内孔区域,用背景色覆盖实现镂空效果 if(holeVAO != 0) { glUniform4f(colorLoc, 0.974f, 0.974f, 0.978f, 1.000f); glBindVertexArray(holeVAO); glDrawArrays(GL_TRIANGLES, 0, holeFillVertices.size()/2); } // 绘制所有轮廓 glUniform4f(colorLoc, 1.0f, 0.0f, 1.0f, 1.0f); glBindVertexArray(outlineVAO); for(const auto& range : drawRanges) glDrawArrays(GL_LINE_STRIP, range.first, range.second); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &outerVAO); glDeleteBuffers(1, &outerVBO); if(holeVAO != 0){ glDeleteVertexArrays(1, &holeVAO); glDeleteBuffers(1, &holeVBO); } glDeleteVertexArrays(1, &outlineVAO); glDeleteBuffers(1, &outlineVBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0;}
三角剖分介绍
以下是关于三角剖分(Triangulation)的简明介绍,涵盖其定义、目标、算法分类及核心应用:
1. 定义
三角剖分指将任意简单多边形(无自交)或点集分解为互不重叠的三角形集合的过程。每个三角形的顶点是原始多边形的顶点,且所有三角形完全覆盖原区域。
2. 核心目标
- 图形渲染基础:为OpenGL/DirectX等图形API提供可光栅化的基本图元(三角形)。
- 数值计算:用于有限元分析(FEA)、流体模拟中的网格生成。
- 几何处理:支持碰撞检测、路径规划、3D建模(曲面离散化)。
3. 关键算法分类
(1) 耳切法(Ear Clipping)
- 适用对象:简单多边形(含凸多边形和凹多边形)。
- 原理:
- 寻找多边形的“耳”(连续三个顶点组成的凸角三角形,且不含其他顶点)。
- 切除该耳(生成一个三角形),更新剩余多边形。
- 重复直至全部三角化。
- 复杂度:O(n²)(可通过双向链表优化至O(n))。
(2) Delaunay 三角剖分
- 适用对象:点集或带约束的多边形。
- 核心性质:
- 空圆特性:任意三角形的外接圆内不包含其他点。
- 最大化最小角:避免“瘦长”三角形,提升数值稳定性。
- 算法:
- 增量法(Bowyer-Watson)
- 分治法
(3) 单调多边形剖分
- 步骤:
- 将多边形分解为多个单调多边形(在某一方向上的边界链单调)。
- 对每个单调多边形进行线性时间三角化。
- 复杂度:O(n log n)。
4. 特殊处理场景
| 情况 | 解决方案 |
|---|---|
| 带孔多边形 | 添加“桥接边”连接内外轮廓,转为简单多边形 |
| 自相交多边形 | 预处理分割为简单多边形 |
| 带约束边 | 约束Delaunay剖分(CDT) |
5. 图形学中的意义
- 硬件友好:GPU对三角形光栅化有高度优化流水线。
- 填充高效:通过重心坐标插值实现颜色/纹理的快速填充。
- 层级简化:复杂模型→三角网格→像素渲染。
代码示例(耳切法伪代码)
def ear_clipping(polygon): triangles = [] vertices = deque(polygon)# 顶点双向队列while len(vertices) >= 3: ear_found = False for i in range(len(vertices)): a, b, c = vertices[i-1], vertices[i], vertices[(i+1) % len(vertices)] if is_convex(a, b, c) and no_vertex_inside(a, b, c, vertices): triangles.append((a, b, c)) vertices.remove(b)# 切除耳顶点b ear_found = True break if not ear_found: break# 避免无限循环(需处理退化情况)return triangles总结
三角剖分是连接矢量几何与栅格化渲染的核心技术,其算法选择取决于:
- 输入类型(点集/多边形/带洞)
- 质量需求(如Delaunay适合数值计算)
- 性能要求(耳切法实现简单,Delaunay更健壮)。
⚠️ 注意:实际应用中需处理退化情况(如共线点)和优化数据结构(如用DCEL存储网格)。
综上,我们解析 e.svg 中的矢量路径点,将其三角剖分为多个三角形,由 OpenGL 驱动 GPU 进行光栅化处理;而 FreeType 字体引擎则可以直接在 CPU 上完成字形轮廓的光栅化。
认识 FreeType
FreeType 简介
FreeType 是一个开源、跨平台的字体引擎库,主要用于解析和渲染字体文件,支持多种字体格式。它被广泛应用于图形界面、游戏、嵌入式系统等场景,提供高效的字体渲染能力。
核心特点
- 跨平台支持
兼容 Windows、Linux、macOS、Android/iOS 等系统,适用于多种开发环境。
- 广泛的字体格式支持
包括 TrueType (TTF)、OpenType (OTF)、Type 1 (PostScript)、WOFF、WOFF2、BDF、PCF 等。
- 高质量的字体渲染
- 支持抗锯齿(Anti-aliasing)和子像素渲染(Subpixel Rendering)。
- 可调整渲染参数(如分辨率、字体大小、Hinting 配置)。
- 灵活的 API
提供 C 语言接口,易于集成到不同编程语言(如 Python、Java 通过 JNI 绑定)。
- 轻量高效
代码精简,适合资源受限的嵌入式设备或实时渲染场景。
核心功能
- 字体文件解析
读取字体文件元数据(如字体名称、字符集信息)和字形数据。
- 字形(Glyph)渲染
- 将字符转换为位图或矢量图形。
- 支持字符缩放、旋转等变换。
- 字体管理
支持多字体文件加载,动态切换字体风格(如粗体、斜体)。
- 高级排版特性(需配合其他库)
复杂文本布局(如阿拉伯文连字)通常需要结合 HarfBuzz 等库实现。
典型应用场景
- 图形界面开发:如 Qt、GTK 等 GUI 框架底层使用 FreeType 渲染文本。
- 游戏引擎:Unity、Unreal Engine 等通过 FreeType 实现游戏内文字显示。
- 嵌入式系统:电子书阅读器、智能手表等设备的字体支持。
- 文档处理:PDF 生成(如 Poppler)、Office 软件中的文本渲染。
简单使用示例(C 语言)
#include <ft2build.h>#include FT_FREETYPE_Hint main() { FT_Library library; FT_Face face; // 初始化库FT_Init_FreeType(&library); // 加载字体文件(如 "arial.ttf")FT_New_Face(library, "font.ttf", 0, &face); // 设置字号(48像素)FT_Set_Pixel_Sizes(face, 0, 48); // 加载字符 'A' 的字形FT_Load_Char(face, 'A', FT_LOAD_RENDER); // 获取位图数据(face->glyph->bitmap)// ...(渲染到屏幕或图像) // 释放资源FT_Done_Face(face); FT_Done_FreeType(library); return 0;}与其他库的协作
- HarfBuzz:处理复杂文本布局(如双向文本、连字)。
- SDL/OpenGL:将渲染后的字形位图显示到屏幕。
- FontConfig(Linux):管理系统字体路径和匹配。
许可证
FreeType 使用 BSD-like 许可证,允许免费用于商业或开源项目。
学习资源
- 官网:www.freetype.org
- GitHub:freetype/freetype
- 文档:API 参考手册及示例代码。
如果需要更深入的技术细节(如自定义渲染、性能优化),可以进一步探讨! 🎨
以前做 Android 开发时,只知道 Skia 库里面对字体的处理使用的 FreeType,这可能是我学习的第一个纯 C 语言开发的库,先通过一个示例认识 FreeType。
💭 网上有大量利用 AI 工具加速学习开源库的教程,其在提升学习效率方面成效显著。就个人学习策略而言,面对庞大的开源代码库时,我倾向于采用问题驱动的方式:聚焦源码解决特定问题的核心思路及其背后的实现原理,而非通读所有细节——永远不要试图煮沸大海,否则你会精疲力竭。
至于高级语法、设计模式等知识,实则是在特定规则约束下,为优雅解决问题而自然衍生的编码成果,无需刻意学习(比如在 Android 面试中,常被问到 OkHttp 或 Retrofit 使用了什么设计模式,这其实有些本末倒置了)。
示例程序:使用 FreeType 库加载 TrueType 字体
#include <iostream>#include <ft2build.h>#include FT_FREETYPE_H #define STB_IMAGE_WRITE_IMPLEMENTATION#include "include/stb_image_write.h" /** * 思路: * 1. 使用 freetype 库加载字体 * 2. 使用 stb_image_write 库保存位图为 PNG 格式 * 3. 将 FT_Bitmap 转换为 RGBA 格式(带透明度) * 4. 将 FT_Bitmap 转换为 灰度格式(单通道) */unsigned char *convert_to_rgba(const FT_Bitmap &bitmap){ const int width = bitmap.width; const int height = bitmap.rows; unsigned char *rgba = new unsigned char[width * height * 4]; // RGBA 缓冲区 for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { // 原始灰度值(0-255) unsigned char gray = bitmap.buffer[y * bitmap.pitch + x]; // 转换为 RGBA(白色文字,黑色透明背景) rgba[(y * width + x) * 4 + 0] = 255; // R rgba[(y * width + x) * 4 + 1] = 255; // G rgba[(y * width + x) * 4 + 2] = 255; // B rgba[(y * width + x) * 4 + 3] = gray; // A(透明度) } } return rgba;} int main(){ FT_Library library; FT_Face face; // 初始化 FreeType 库 if (FT_Init_FreeType(&library)) { std::cerr << "无法初始化 FreeType 库" << std::endl; return 1; } // 加载字体文件(请替换为你的字体路径) std::cout << "CMake Source Dir: " << SOURCE_DIR << std::endl; std::string fontPathStr = std::string(SOURCE_DIR) + "/assets/FreeSerifItalic.ttf"; const char *fontPath = fontPathStr.c_str(); if (FT_New_Face(library, fontPath, 0, &face)) { std::cerr << "无法加载字体文件: " << fontPath << std::endl; FT_Done_FreeType(library); return 1; } // 设置字体大小(48像素) FT_Set_Pixel_Sizes(face, 0, 24); // 加载字符 'e' 的字形 if (FT_Load_Char(face, 'e', FT_LOAD_RENDER)) { std::cerr << "无法加载字符字形" << std::endl; FT_Done_Face(face); FT_Done_FreeType(library); return 1; } // 获取字形位图信息 FT_Bitmap bitmap = face->glyph->bitmap; // 保存 btimap 为 .png 文件 // 打印位图信息 std::cout << "字符 'A' 的位图信息:\n" << "宽度: " << bitmap.width << " 像素\n" << "高度: " << bitmap.rows << " 像素\n" << "像素模式: " << (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY ? "8位灰度" : "其他") << std::endl; // 转换为 RGBA unsigned char *rgba_data = convert_to_rgba(bitmap); // 保存为 PNG(反轉 Y 軸以符合图像坐标系) stbi_flip_vertically_on_write(1); stbi_write_png("output.png", bitmap.width, bitmap.rows, 4, // RGBA 通道数 rgba_data, bitmap.width * 4); // 每行字节数 // 直接保存灰度数据(无需转换) stbi_write_png("gray.png", bitmap.width, bitmap.rows, 1, // 单通道 bitmap.buffer, bitmap.pitch); // 注意使用 pitch 而非 width // 清理资源 FT_Done_Face(face); FT_Done_FreeType(library); return 0;}使用 FreeType 解析出字符 e 的轮廓,并通过 OpenGl 绘制其轮廓。
示例程序:使用 FreeType 库加载 TrueType 字体,并绘制字符 ‘e’ 轮廓
#include <iostream>#include <math.h>#include <vector>#include <string>#include <cctype> #include <glad/glad.h>#include <GLFW/glfw3.h> // FreeType 头文件#include <ft2build.h>#include FT_FREETYPE_H#include FT_OUTLINE_H /** * 思路: * 1. 使用 freetype 库加载字体 * 2. 解析轮廓数据 * 3. 使用 opengl 绘制轮廓 */using namespace std; // 用于保存轮廓顶点坐标的结构体struct Vertex{ float x, y;}; // 全局容器:保存所有解构后的轮廓数据,每个 contour 为一组连续顶点std::vector<std::vector<Vertex>> gContours; // ----------------- FT_Outline_Decompose 回调函数 ----------------- //// 起始点回调:为新的轮廓创建一个顶点数组int move_to_callback(const FT_Vector *to, void *user){ auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user); contours->push_back(std::vector<Vertex>()); Vertex v; // 将 26.6 定点数转换为浮点数 v.x = to->x / 64.0f; v.y = to->y / 64.0f; contours->back().push_back(v); return 0;} // 直线段回调:直接添加目标点到当前轮廓int line_to_callback(const FT_Vector *to, void *user){ auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user); // 上一个点 Vertex last = contours->back()[contours->back().size() - 1]; Vertex v; v.x = to->x / 64.0f; v.y = to->y / 64.0f; contours->back().push_back(v); return 0;} // 二次贝塞尔曲线回调:使用均匀分段方法近似曲线int conic_to_callback(const FT_Vector *control, const FT_Vector *to, void *user){ auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user); // 上一个点 Vertex from = contours->back().back(); const int STEPS = 5; // 分段数,可根据需求调整 for (int i = 1; i <= STEPS; i++) { float t = i / (float)STEPS; float one_minus_t = 1.0f - t; Vertex v; v.x = one_minus_t * one_minus_t * from.x + 2 * one_minus_t * t * (control->x / 64.0f) + t * t * (to->x / 64.0f); v.y = one_minus_t * one_minus_t * from.y + 2 * one_minus_t * t * (control->y / 64.0f) + t * t * (to->y / 64.0f); contours->back().push_back(v); } return 0;} // 三次贝塞尔曲线回调,同样均匀分段int cubic_to_callback(const FT_Vector *control1, const FT_Vector *control2, const FT_Vector *to, void *user){ auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user); Vertex from = contours->back().back(); const int STEPS = 20; for (int i = 1; i <= STEPS; i++) { float t = i / (float)STEPS; float one_minus_t = 1.0f - t; Vertex v; v.x = one_minus_t * one_minus_t * one_minus_t * from.x + 3 * one_minus_t * one_minus_t * t * (control1->x / 64.0f) + 3 * one_minus_t * t * t * (control2->x / 64.0f) + t * t * t * (to->x / 64.0f); v.y = one_minus_t * one_minus_t * one_minus_t * from.y + 3 * one_minus_t * one_minus_t * t * (control1->y / 64.0f) + 3 * one_minus_t * t * t * (control2->y / 64.0f) + t * t * t * (to->y / 64.0f); contours->back().push_back(v); } return 0;} // 组装 FT_Outline_Decompose 需要的回调接口FT_Outline_Funcs ftCallbacks = { move_to_callback, line_to_callback, conic_to_callback, cubic_to_callback, 0, // shift 0 // delta}; // 顶点着色器const char *vertexShaderSource = R"( #version 330 core layout(location = 0) in vec2 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); } )"; // 片段着色器const char *fragmentShaderSource = R"( #version 330 core out vec4 FragColor; void main() { FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色点 } )"; // 编译着色器GLuint compileShader(GLenum type, const char *source){ GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint success; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { char infoLog[512]; glGetShaderInfoLog(shader, 512, nullptr, infoLog); std::cerr << "ERROR: SHADER COMPILATION\n" << infoLog << std::endl; } return shader;} // 创建着色器程序GLuint createShaderProgram(){ GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource); GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); GLuint shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); return shaderProgram;} int main(){ FT_Library library; FT_Face face; // 初始化 FreeType 库 if (FT_Init_FreeType(&library)) { std::cerr << "无法初始化 FreeType 库" << std::endl; return 1; } // 加载字体文件(请替换为你的字体路径) std::string fontPathStr = std::string(SOURCE_DIR) + "/assets/FreeSerifItalic.ttf"; const char *fontPath = fontPathStr.c_str(); if (FT_New_Face(library, fontPath, 0, &face)) { std::cerr << "无法加载字体文件: " << fontPath << std::endl; FT_Done_FreeType(library); return 1; } FT_Set_Pixel_Sizes(face, 0, 48); if (FT_Load_Char(face, 'e', FT_LOAD_NO_BITMAP)) { std::cerr << "无法加载字符字形" << std::endl; FT_Done_Face(face); FT_Done_FreeType(library); return 1; } // 清空并保存轮廓数据 gContours.clear(); if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE) { FT_Outline_Decompose(&face->glyph->outline, &ftCallbacks, &gContours); } else { std::cerr << "ERROR: Glyph does not contain an outline" << std::endl; return EXIT_FAILURE; } // 获取字形位图信息 FT_Bitmap bitmap = face->glyph->bitmap; // 打印位图信息 std::cout << "字符 'e' 的位图信息:\n" << "宽度: " << bitmap.width << " 像素\n" << "高度: " << bitmap.rows << " 像素\n" << "像素模式: " << (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY ? "8位灰度" : "其他") << std::endl; // 清理资源 FT_Done_Face(face); FT_Done_FreeType(library); // ---------------------- GLFW 与 GLAD 初始化 ---------------------- // std::vector<float> lines; // 通过偏移、缩放来调整这些点,使用显示在窗口内 float offsetX = -0.5; float offsetY = -0.5; float scale = 20.0f; for (const auto &contour : gContours) { std::vector<Vertex> tempPoints = contour; for (size_t i = 0; i < tempPoints.size() - 1; ++i) { lines.push_back(tempPoints[i].x / scale + offsetX); lines.push_back(tempPoints[i].y / scale + offsetY); lines.push_back(tempPoints[i + 1].x / scale + offsetX); lines.push_back(tempPoints[i + 1].y / scale + offsetY); } } // 打印 lines 中的点 std::cout << "Extracted Points (excluding Q/q control points):\n"; for (size_t i = 0; i < lines.size(); i += 2) { std::cout << "(" << lines[i] << ", " << lines[i + 1] << ")\n"; } if (!glfwInit()) { std::cerr << "无法初始化 GLFW" << std::endl; return -1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGL Render e.svg Lines", nullptr, nullptr); if (!window) { std::cerr << "无法创建窗口" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cerr << "无法初始化 GLAD" << std::endl; glfwTerminate(); return -1; } GLuint shaderProgram = createShaderProgram(); GLuint VAO, VBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(float), lines.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0); glEnableVertexAttribArray(0); glPointSize(4.0f); while (!glfwWindowShouldClose(window)) { glClearColor(0.974f, 0.974f, 0.978f, 1.000f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_LINES, 0, lines.size() / 2); glDrawArrays(GL_POINTS, 0, lines.size() / 2); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteProgram(shaderProgram); glfwDestroyWindow(window); glfwTerminate(); return 0;}
试试 AlibabaPuHuiTi-3-55-Regular.ttf 字体

也可以把解析出的点输出到 Mathematica 显示。
示例程序:输出 ‘e’ 轮廓到 Mathematica(加载标志使用 FT_LOAD_NO_SCALE,输出到 Mathematica 不进行 26.6 定点数转换)
#include <iostream>#include <math.h>#include <vector>#include <string>#include <cctype> #include <glad/glad.h>#include <GLFW/glfw3.h> // FreeType 头文件#include <ft2build.h>#include FT_FREETYPE_H#include FT_OUTLINE_H /** * 思路: * 1. 使用 freetype 库加载字体 * 2. 解析轮廓数据 * 2.1 输出到 mathematica * 3. 使用 opengl 绘制轮廓 */using namespace std; // 用于保存轮廓顶点坐标的结构体struct Vertex{ float x, y;}; std::vector<std::string> graphics; // 全局容器:保存所有解构后的轮廓数据,每个 contour 为一组连续顶点std::vector<std::vector<Vertex>> gContours; // ----------------- FT_Outline_Decompose 回调函数 ----------------- //// 起始点回调:为新的轮廓创建一个顶点数组int move_to_callback(const FT_Vector *to, void *user){ auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user); contours->push_back(std::vector<Vertex>()); Vertex v; // 将 26.6 定点数转换为浮点数 v.x = to->x / 64.0f; v.y = to->y / 64.0f; contours->back().push_back(v); return 0;} // 直线段回调:直接添加目标点到当前轮廓int line_to_callback(const FT_Vector *to, void *user){ auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user); // 上一个点 Vertex last = contours->back()[contours->back().size() - 1]; Vertex v; v.x = to->x / 64.0f; v.y = to->y / 64.0f; contours->back().push_back(v); graphics.push_back("Line[{{" + std::to_string(last.x * 64.0f) + ", " + std::to_string(last.y * 64.0f) + "}, {" + std::to_string(v.x * 64.0f) + ", " + std::to_string(v.y * 64.0f) + "}}], \n"); return 0;} // 二次贝塞尔曲线回调:使用均匀分段方法近似曲线int conic_to_callback(const FT_Vector *control, const FT_Vector *to, void *user){ auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user); // 上一个点 Vertex from = contours->back().back(); std::string str = "BezierCurve[{{" + std::to_string(from.x * 64.0f) + ", " + std::to_string(from.y * 64.0f) + "}, {" + std::to_string(control->x) + ", " + std::to_string(control->y) + "}, {" + std::to_string(to->x) + ", " + std::to_string(to->y) + "}}], \n"; graphics.push_back(str); const int STEPS = 1; // 分段数,可根据需求调整 for (int i = 1; i <= STEPS; i++) { float t = i / (float)STEPS; float one_minus_t = 1.0f - t; Vertex v; v.x = one_minus_t * one_minus_t * from.x + 2 * one_minus_t * t * (control->x / 64.0f) + t * t * (to->x / 64.0f); v.y = one_minus_t * one_minus_t * from.y + 2 * one_minus_t * t * (control->y / 64.0f) + t * t * (to->y / 64.0f); contours->back().push_back(v); } return 0;} // 三次贝塞尔曲线回调,同样均匀分段int cubic_to_callback(const FT_Vector *control1, const FT_Vector *control2, const FT_Vector *to, void *user){ auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user); Vertex from = contours->back().back(); const int STEPS = 20; for (int i = 1; i <= STEPS; i++) { float t = i / (float)STEPS; float one_minus_t = 1.0f - t; Vertex v; v.x = one_minus_t * one_minus_t * one_minus_t * from.x + 3 * one_minus_t * one_minus_t * t * (control1->x / 64.0f) + 3 * one_minus_t * t * t * (control2->x / 64.0f) + t * t * t * (to->x / 64.0f); v.y = one_minus_t * one_minus_t * one_minus_t * from.y + 3 * one_minus_t * one_minus_t * t * (control1->y / 64.0f) + 3 * one_minus_t * t * t * (control2->y / 64.0f) + t * t * t * (to->y / 64.0f); contours->back().push_back(v); } return 0;} // 组装 FT_Outline_Decompose 需要的回调接口FT_Outline_Funcs ftCallbacks = { move_to_callback, line_to_callback, conic_to_callback, cubic_to_callback, 0, // shift 0 // delta}; // 顶点着色器const char *vertexShaderSource = R"( #version 330 core layout(location = 0) in vec2 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); } )"; // 片段着色器const char *fragmentShaderSource = R"( #version 330 core out vec4 FragColor; void main() { FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色点 } )"; // 编译着色器GLuint compileShader(GLenum type, const char *source){ GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); GLint success; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); if (!success) { char infoLog[512]; glGetShaderInfoLog(shader, 512, nullptr, infoLog); std::cerr << "ERROR: SHADER COMPILATION\n" << infoLog << std::endl; } return shader;} // 创建着色器程序GLuint createShaderProgram(){ GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource); GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource); GLuint shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); return shaderProgram;} int main(){ FT_Library library; FT_Face face; // 初始化 FreeType 库 if (FT_Init_FreeType(&library)) { std::cerr << "无法初始化 FreeType 库" << std::endl; return 1; } // 加载字体文件(请替换为你的字体路径) std::string fontPathStr = std::string(SOURCE_DIR) + "/assets/FreeSerifItalic.ttf"; const char *fontPath = fontPathStr.c_str(); if (FT_New_Face(library, fontPath, 0, &face)) { std::cerr << "无法加载字体文件: " << fontPath << std::endl; FT_Done_FreeType(library); return 1; } // 设置字体大小(48像素) FT_Set_Pixel_Sizes(face, 0, 48); // if (FT_Load_Char(face, 'e', FT_LOAD_NO_BITMAP)) if (FT_Load_Char(face, 'e', FT_LOAD_NO_SCALE)) { std::cerr << "无法加载字符字形" << std::endl; FT_Done_Face(face); FT_Done_FreeType(library); return 1; } graphics.push_back("Graphics[{\n"); // 清空并保存轮廓数据 gContours.clear(); if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE) { FT_Outline_Decompose(&face->glyph->outline, &ftCallbacks, &gContours); } else { std::cerr << "ERROR: Glyph does not contain an outline" << std::endl; return EXIT_FAILURE; } graphics.push_back("\n}, Axes -> True]"); // 获取字形位图信息 FT_Bitmap bitmap = face->glyph->bitmap; // 打印位图信息 std::cout << "字符 'e' 的位图信息:\n" << "宽度: " << bitmap.width << " 像素\n" << "高度: " << bitmap.rows << " 像素\n" << "像素模式: " << (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY ? "8位灰度" : "其他") << std::endl; // 清理资源 FT_Done_Face(face); FT_Done_FreeType(library); // ---------------------- GLFW 与 GLAD 初始化 ---------------------- // // 打印 graphics std::cout << "Graphics:\n"; for (const auto &g : graphics) { std::cout << g << std::endl; } std::vector<float> lines; // 通过偏移、缩放来调整这些点,使用显示在窗口内 float offsetX = -0.5; float offsetY = -0.5; float scale = 20.0f; for (const auto &contour : gContours) { std::vector<Vertex> tempPoints = contour; for (size_t i = 0; i < tempPoints.size() - 1; ++i) { lines.push_back(tempPoints[i].x / scale + offsetX); lines.push_back(tempPoints[i].y / scale + offsetY); lines.push_back(tempPoints[i + 1].x / scale + offsetX); lines.push_back(tempPoints[i + 1].y / scale + offsetY); } } // 打印 lines 中的点 std::cout << "Extracted Points (excluding Q/q control points):\n"; for (size_t i = 0; i < lines.size(); i += 2) { std::cout << "(" << lines[i] << ", " << lines[i + 1] << ")\n"; } if (!glfwInit()) { std::cerr << "无法初始化 GLFW" << std::endl; return -1; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow *window = glfwCreateWindow(800, 800, "OpenGL Render e.svg Points", nullptr, nullptr); if (!window) { std::cerr << "无法创建窗口" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cerr << "无法初始化 GLAD" << std::endl; glfwTerminate(); return -1; } GLuint shaderProgram = createShaderProgram(); GLuint VAO, VBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(float), lines.data(), GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0); glEnableVertexAttribArray(0); glPointSize(4.0f); while (!glfwWindowShouldClose(window)) { glClearColor(1.0f, 1.0f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_LINES, 0, lines.size() / 2); glDrawArrays(GL_POINTS, 0, lines.size() / 2); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteProgram(shaderProgram); glfwDestroyWindow(window); glfwTerminate(); return 0;}输出结果如下
Graphics[{ Line[{{358.000000, 109.000000}, {370.000000, 97.000000}}], BezierCurve[{{370.000000, 97.000000}, {326, 46}, {271, 17}}], BezierCurve[{{271.000000, 17.000000}, {216, -11}, {163, -11}}], BezierCurve[{{163.000000, -11.000000}, {104, -11}, {67, 27}}], BezierCurve[{{67.000000, 27.000000}, {31, 65}, {31, 126}}], BezierCurve[{{31.000000, 126.000000}, {31, 199}, {76, 273}}], BezierCurve[{{76.000000, 273.000000}, {121, 347}, {191, 394}}], BezierCurve[{{191.000000, 394.000000}, {262, 441}, {332, 441}}], BezierCurve[{{332.000000, 441.000000}, {369, 441}, {390, 423}}], BezierCurve[{{390.000000, 423.000000}, {412, 405}, {412, 373}}], BezierCurve[{{412.000000, 373.000000}, {412, 308}, {340, 257}}], BezierCurve[{{340.000000, 257.000000}, {268, 206}, {156, 190}}], Line[{{156.000000, 190.000000}, {128.000000, 186.000000}}], BezierCurve[{{128.000000, 186.000000}, {118, 146}, {118, 124}}], BezierCurve[{{118.000000, 124.000000}, {118, 83}, {143, 58}}], BezierCurve[{{143.000000, 58.000000}, {169, 34}, {212, 34}}], BezierCurve[{{212.000000, 34.000000}, {244, 34}, {274, 49}}], BezierCurve[{{274.000000, 49.000000}, {304, 65}, {358, 109}}], BezierCurve[{{135.000000, 208.000000}, {189, 221}, {229, 240}}], BezierCurve[{{229.000000, 240.000000}, {270, 259}, {291, 277}}], BezierCurve[{{291.000000, 277.000000}, {313, 295}, {326, 316}}], BezierCurve[{{326.000000, 316.000000}, {339, 337}, {342, 351}}], BezierCurve[{{342.000000, 351.000000}, {346, 365}, {346, 379}}], BezierCurve[{{346.000000, 379.000000}, {346, 396}, {336, 407}}], BezierCurve[{{336.000000, 407.000000}, {327, 418}, {311, 418}}], BezierCurve[{{311.000000, 418.000000}, {273, 418}, {225, 368}}], BezierCurve[{{225.000000, 368.000000}, {177, 319}, {152, 252}}], Line[{{152.000000, 252.000000}, {135.000000, 208.000000}}], }, Axes -> True]
对比字体编辑软件打开的字符 e,这些点的坐标都是一致的,例如,第一个点的坐标(358.000000, 109.000000)。

不使用 FreeType,让 AI 帮我们生成一份解析 TureType 字体的代码,打印字符e的坐标点跟上面也是一样的。
示例程序:解析 TrueType 字体,打印字符 ‘e’ 轮廓点
#include <iostream>#include <fstream>#include <cstdint>#include <vector>#include <cstring> // 总结:// 1. 该代码解析了 TrueType 字体文件(TTF)的结构,读取了 Offset Table、各个表记录(Table Records)以及 cmap、head、loca 和 glyf 表。// 2. 通过 cmap 表查找字符 'e' 的 glyph index,并解析 glyf 表获取该 glyph 的轮廓数据。// 3. 解析了 glyf 表中的简单字形(simple glyph)轮廓数据,包括点的坐标和 on-curve/off-curve 标记。// 4. 最终输出了字符 'e' 的轮廓点数据以及轮廓划分信息。 // 辅助函数:从文件中读取大端格式数据uint16_t readU16(std::ifstream &ifs){ uint8_t bytes[2]; ifs.read(reinterpret_cast<char *>(bytes), 2); return (bytes[0] << 8) | bytes[1];} int16_t readS16(std::ifstream &ifs){ return static_cast<int16_t>(readU16(ifs));} uint32_t readU32(std::ifstream &ifs){ uint8_t bytes[4]; ifs.read(reinterpret_cast<char *>(bytes), 4); return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];} uint8_t readU8(std::ifstream &ifs){ char byte; ifs.get(byte); return static_cast<uint8_t>(byte);} // TableRecord:记录 TTF 文件中每个表的信息(tag、checkSum、offset、length)struct TableRecord{ char tag[5]; // 4 字节字符 + 结束符 uint32_t checkSum; uint32_t offset; uint32_t length;}; int main(){ std::string fontPathStr = std::string(SOURCE_DIR) + "/assets/FreeSerifItalic.ttf"; const char *filename = fontPathStr.c_str(); std::ifstream file(filename, std::ios::binary); if (!file) { std::cerr << "无法打开 TTF 文件: " << filename << std::endl; return 1; } // 1. 解析 Offset Table(12 字节) uint32_t sfntVersion = readU32(file); uint16_t numTables = readU16(file); uint16_t searchRange = readU16(file); uint16_t entrySelector = readU16(file); uint16_t rangeShift = readU16(file); // 2. 读取所有 Table Records std::vector<TableRecord> tables; for (int i = 0; i < numTables; i++) { TableRecord rec; file.read(rec.tag, 4); rec.tag[4] = '\0'; rec.checkSum = readU32(file); rec.offset = readU32(file); rec.length = readU32(file); tables.push_back(rec); } // 辅助 lambda:通过 tag 查找对应表记录 auto findTable = [&tables](const char *tag) -> TableRecord * { for (auto &t : tables) { if (std::strcmp(t.tag, tag) == 0) return &t; } return nullptr; }; TableRecord *cmapTable = findTable("cmap"); TableRecord *headTable = findTable("head"); TableRecord *locaTable = findTable("loca"); TableRecord *glyfTable = findTable("glyf"); if (!cmapTable || !headTable || !locaTable || !glyfTable) { std::cerr << "未找到必要的表(cmap/head/loca/glyf)。" << std::endl; return 1; } // 3. 解析 head 表:获取 indexToLocFormat(位于 head 表偏移 50 处) file.seekg(headTable->offset + 50, std::ios::beg); int16_t indexToLocFormat = readS16(file); // std::cout << "indexToLocFormat = " << indexToLocFormat << std::endl; // 4. 解析 cmap 表:根据字符 'e' (Unicode 101) 查找对应的 glyph index file.seekg(cmapTable->offset, std::ios::beg); uint16_t cmapVersion = readU16(file); uint16_t numSubtables = readU16(file); uint32_t targetSubtableOffset = 0; // 遍历 subtables,查找平台 ID 为 3、编码 ID 为 1 且 format 为 4 的子表 for (int i = 0; i < numSubtables; i++) { uint16_t platformID = readU16(file); uint16_t encodingID = readU16(file); uint32_t subtableOffset = readU32(file); std::streampos currPos = file.tellg(); // 跳转到该子表检查 format 字段 file.seekg(cmapTable->offset + subtableOffset, std::ios::beg); uint16_t format = readU16(file); if (platformID == 3 && encodingID == 1 && format == 4) { targetSubtableOffset = subtableOffset; file.seekg(currPos, std::ios::beg); break; } file.seekg(currPos, std::ios::beg); } if (targetSubtableOffset == 0) { std::cerr << "未找到合适的 cmap 子表(平台 3,编码 1,format 4)。" << std::endl; return 1; } // 跳转到选定的 cmap 子表,并解析 format 4 数据 file.seekg(cmapTable->offset + targetSubtableOffset, std::ios::beg); uint16_t format = readU16(file); // 应为 4 uint16_t length = readU16(file); uint16_t language = readU16(file); uint16_t segCountX2 = readU16(file); uint16_t segCount = segCountX2 / 2; uint16_t searchRange_cmap = readU16(file); uint16_t entrySelector_cmap = readU16(file); uint16_t rangeShift_cmap = readU16(file); std::vector<uint16_t> endCodes(segCount); for (int i = 0; i < segCount; i++) { endCodes[i] = readU16(file); } uint16_t reservedPad = readU16(file); std::vector<uint16_t> startCodes(segCount); for (int i = 0; i < segCount; i++) { startCodes[i] = readU16(file); } std::vector<int16_t> idDeltas(segCount); for (int i = 0; i < segCount; i++) { idDeltas[i] = readS16(file); } std::vector<uint16_t> idRangeOffsets(segCount); for (int i = 0; i < segCount; i++) { idRangeOffsets[i] = readU16(file); } // 对于字符 'e' (Unicode 101)进行查找 uint16_t unicode = 101; uint16_t glyphIndex = 0; for (int i = 0; i < segCount; i++) { if (unicode >= startCodes[i] && unicode <= endCodes[i]) { if (idRangeOffsets[i] == 0) { glyphIndex = (unicode + idDeltas[i]) % 65536; } else { std::streampos pos = file.tellg(); std::streampos glyphIndexAddr = cmapTable->offset + targetSubtableOffset + 14 + (i * 2) + idRangeOffsets[i] + (unicode - startCodes[i]) * 2; file.seekg(glyphIndexAddr, std::ios::beg); glyphIndex = readU16(file); if (glyphIndex != 0) glyphIndex = (glyphIndex + idDeltas[i]) % 65536; file.seekg(pos, std::ios::beg); } break; } } std::cout << "字符 'e' 对应的 Glyph Index = " << glyphIndex << std::endl; // 5. 解析 loca 表:获取 glyph 在 glyf 表中的偏移 uint32_t glyphOffset, glyphNextOffset; if (indexToLocFormat == 0) { // short 版本,每个记录为 uint16_t,偏移需要乘 2 file.seekg(locaTable->offset + glyphIndex * 2, std::ios::beg); uint16_t off = readU16(file); glyphOffset = off * 2; file.seekg(locaTable->offset + (glyphIndex + 1) * 2, std::ios::beg); uint16_t off2 = readU16(file); glyphNextOffset = off2 * 2; } else { // long 版本,每个记录为 uint32_t file.seekg(locaTable->offset + glyphIndex * 4, std::ios::beg); glyphOffset = readU32(file); file.seekg(locaTable->offset + (glyphIndex + 1) * 4, std::ios::beg); glyphNextOffset = readU32(file); } if (glyphOffset == glyphNextOffset) { std::cerr << "该 glyph 不包含轮廓数据。" << std::endl; return 1; } // 6. 解析 glyf 表中 simple glyph(简单字形)的轮廓数据 file.seekg(glyfTable->offset + glyphOffset, std::ios::beg); int16_t numberOfContours = readS16(file); int16_t xMin = readS16(file); int16_t yMin = readS16(file); int16_t xMax = readS16(file); int16_t yMax = readS16(file); if (numberOfContours < 0) { std::cerr << "组合字形(composite glyph)不在本示例支持范围内。" << std::endl; return 1; } // 读取每个轮廓的终点索引 std::vector<uint16_t> endPtsOfContours(numberOfContours); for (int i = 0; i < numberOfContours; i++) { endPtsOfContours[i] = readU16(file); } // 跳过 TrueType 指令 uint16_t instructionLength = readU16(file); file.seekg(instructionLength, std::ios::cur); int numPoints = endPtsOfContours.back() + 1; // 读取每个点的 flags,注意存在重复标记机制 std::vector<uint8_t> finalFlags; while (finalFlags.size() < (size_t)numPoints) { uint8_t flag = readU8(file); finalFlags.push_back(flag); if (flag & 0x08) { // 重复标记 uint8_t repeatCount = readU8(file); for (int j = 0; j < repeatCount; j++) { finalFlags.push_back(flag); } } } // 读取 x 坐标(增量编码) std::vector<int16_t> xCoordinates(numPoints, 0); int16_t x = 0; for (int i = 0; i < numPoints; i++) { if (finalFlags[i] & 0x02) { // x-short vector:一个 unsigned byte uint8_t dx = readU8(file); if (finalFlags[i] & 0x10) { // 若设置,则为正值 x += dx; } else { x -= dx; } } else { if (!(finalFlags[i] & 0x10)) { // x not same int16_t dx = readS16(file); x += dx; } } xCoordinates[i] = x; } // 读取 y 坐标(增量编码) std::vector<int16_t> yCoordinates(numPoints, 0); int16_t y = 0; for (int i = 0; i < numPoints; i++) { if (finalFlags[i] & 0x04) { // y-short vector:一个 unsigned byte uint8_t dy = readU8(file); if (finalFlags[i] & 0x20) { // 若设置,则为正值 y += dy; } else { y -= dy; } } else { if (!(finalFlags[i] & 0x20)) { int16_t dy = readS16(file); y += dy; } } yCoordinates[i] = y; } // 输出轮廓数据 std::cout << "轮廓数据(Glyph 'e'):" << std::endl; std::cout << "Number of Contours: " << numberOfContours << std::endl; std::cout << "Bounding Box: (" << xMin << ", " << yMin << ") to (" << xMax << ", " << yMax << ")" << std::endl; std::cout << "轮廓终点索引: "; for (const auto &endPt : endPtsOfContours) { std::cout << endPt << " "; } std::cout << std::endl; std::cout << "点坐标(x, y)及标志:" << std::endl; for (int i = 0; i < numPoints; i++) { std::cout << "Point " << i << ": (" << xCoordinates[i] << ", " << yCoordinates[i] << "), Flags: " << static_cast<int>(finalFlags[i]) << std::endl; } std::cout << "----------------------------------------" << std::endl; return 0;} 打印输出
字符 'e' 对应的 Glyph Index = 72轮廓数据(Glyph 'e'):Number of Contours: 2Bounding Box: (31, -11) to (412, 441)轮廓终点索引: 25 38 点坐标(x, y)及标志:Point 0: (358, 109), Flags: 37Point 1: (370, 97), Flags: 23Point 2: (326, 46), Flags: 14Point 3: (216, -11), Flags: 14Point 4: (163, -11), Flags: 35Point 5: (104, -11), Flags: 34Point 6: (31, 65), Flags: 38Point 7: (31, 126), Flags: 53Point 8: (31, 199), Flags: 52Point 9: (121, 347), Flags: 62Point 10: (262, 441), Flags: 62Point 11: (332, 441), Flags: 51Point 12: (369, 441), Flags: 50Point 13: (412, 405), Flags: 22Point 14: (412, 373), Flags: 21Point 15: (412, 308), Flags: 20Point 16: (268, 206), Flags: 6Point 17: (156, 190), Flags: 15Point 18: (128, 186), Flags: 15Point 19: (118, 146), Flags: 6Point 20: (118, 124), Flags: 21Point 21: (118, 83), Flags: 20Point 22: (169, 34), Flags: 22Point 23: (212, 34), Flags: 51Point 24: (244, 34), Flags: 50Point 25: (304, 65), Flags: 54Point 26: (135, 208), Flags: 39Point 27: (189, 221), Flags: 62Point 28: (270, 259), Flags: 62Point 29: (313, 295), Flags: 62Point 30: (339, 337), Flags: 62Point 31: (346, 365), Flags: 62Point 32: (346, 379), Flags: 53Point 33: (346, 396), Flags: 52Point 34: (327, 418), Flags: 38Point 35: (311, 418), Flags: 35Point 36: (273, 418), Flags: 34Point 37: (177, 319), Flags: 6Point 38: (152, 252), Flags: 7💭 .ttf 文件本质上是一种结构化二进制格式,其解析流程与图像、音频等媒体文件相似,首先读取文件头(Header),根据头信息定位各数据段在内存中的偏移量,进而加载对应的字形轮廓及元数据。
FreeType 字符轮廓的光栅化
在前面的内容中,我们已经了解了如何解析 .ttf 文件以获取对应字符的轮廓数据。接下来的问题是如何将这些轮廓数据进行光栅化,或者说怎么从数学描述转换到像素阵列,FreeType 中可以通过导出灰度图的方式来探索这个过程。
调试 FreeType 源码
要想明晰这个过程具体细节,不得不深入到源码进行查看,克隆 freetype 源码,修改 CMakeList.txt 文件,把导出静态库的配置修改为编译可执行项目。
# add_library(freetype# ${PUBLIC_HEADERS}# ${PUBLIC_CONFIG_HEADERS}# ${PRIVATE_HEADERS}# ${BASE_SRCS}# ) add_executable(freetype-test main.cpp ${PUBLIC_HEADERS} ${PUBLIC_CONFIG_HEADERS} ${PRIVATE_HEADERS} ${BASE_SRCS})添加一个 main.cpp 文件
#include <ft2build.h>#include <iostream>#include FT_FREETYPE_H /** * 调试用 * 1. 使用 freetype 库加载字体 * 2. 打印位图内容 */int main(){ FT_Library library; FT_Face face; // 初始化 FreeType 库 if (FT_Init_FreeType(&library)) { std::cerr << "无法初始化 FreeType 库" << std::endl; return 1; } // 加载字体文件(请替换为你的字体路径) const char *fontPath ="../assets/FreeSerifItalic.ttf"; if (FT_New_Face(library, fontPath, 0, &face)) { std::cerr << "无法加载字体文件: " << fontPath << std::endl; FT_Done_FreeType(library); return 1; } // 设置字体大小(48像素) FT_Set_Pixel_Sizes(face, 0, 48); // 加载字符 'e' if (FT_Load_Char(face, 'e', FT_LOAD_RENDER)) { std::cerr << "无法加载字符字形" << std::endl; FT_Done_Face(face); FT_Done_FreeType(library); return 1; } // 获取字形位图信息 FT_Bitmap bitmap = face->glyph->bitmap; // 打印位图信息 std::cout << "字符 'A' 的位图信息:\n" << "宽度: " << bitmap.width << " 像素\n" << "高度: " << bitmap.rows << " 像素\n" << "像素模式: " << (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY ? "8位灰度" : "其他") << std::endl; // 打印位图内容(ASCII) std::cout << "\n位图内容:\n"; for (unsigned int y = 0; y < bitmap.rows; ++y) { for (unsigned int x = 0; x < bitmap.width; ++x) { unsigned char pixel = bitmap.buffer[y * bitmap.pitch + x]; std::cout << (pixel > 128 ? '*' : ' '); // 用不同字符表示灰度值 } std::cout << '\n'; } // 清理资源 FT_Done_Face(face); FT_Done_FreeType(library); return 0;}💡 可以设置 freetype 日志级别,打印日志到控制台,方便我们进行调试。
你无法调试你从未理解过的东西。一些前置知识是必须的:计算机图形学 多边形扫描填充 (bilibili)、CG11-光栅化1-扫描填充 (bilibili)、计算机图形学|扫描线算法 ET表 AET表例题讲解 (bilibil)。
示例代码:扫描线填充算法
#include <iostream>#include <vector>#include <algorithm>#include <cmath>#include <iostream>#include <math.h>#include <vector>#include <string>#include <sstream>#include <cctype> /** * 总结: * 1. 该代码实现了一个简单的SVG路径解析器,能够将SVG路径字符串转换为一系列2D点。 * 2. 使用了扫描线填充算法来填充多边形区域。 */ using namespace std; // 2D 点结构struct Point{ float x, y;}; // 一条子路径:按照顺序排列的点using PathSegment = std::vector<Point>; std::string normalizePathString(const std::string &path){ std::string result; for (size_t i = 0; i < path.size(); i++) { char c = path[i]; if (std::isalpha(c)) { result.push_back(' '); result.push_back(c); result.push_back(' '); } else { result.push_back(c); } } return result;} std::vector<PathSegment> parseSVGPath(const std::string &pathDataIn){ std::string pathData = normalizePathString(pathDataIn); std::vector<PathSegment> segments; PathSegment currentSegment; Point current = {0.0f, 0.0f}; Point startPoint = {0.0f, 0.0f}; std::istringstream stream(pathData); std::string token; while (stream >> token) { char cmd = token[0]; switch (cmd) { case 'M': case 'm': { float x, y; stream >> x >> y; Point p; if (cmd == 'm') { p.x = current.x + x; p.y = current.y + y; } else { p.x = x; p.y = y; } if (!currentSegment.empty()) { segments.push_back(currentSegment); currentSegment.clear(); } current = p; startPoint = p; currentSegment.push_back(p); break; } case 'L': case 'l': { float x, y; stream >> x >> y; Point p; if (cmd == 'l') { p.x = current.x + x; p.y = current.y + y; } else { p.x = x; p.y = y; } current = p; currentSegment.push_back(p); break; } case 'Q': case 'q': { float cx, cy, ex, ey; stream >> cx >> cy >> ex >> ey; Point control, end; if (cmd == 'q') { control.x = current.x + cx; control.y = current.y + cy; end.x = current.x + ex; end.y = current.y + ey; } else { control.x = cx; control.y = cy; end.x = ex; end.y = ey; } const int samples = 20; for (int i = 1; i <= samples; i++) { float t = float(i) / samples; float inv = 1.0f - t; float x = inv * inv * current.x + 2 * inv * t * control.x + t * t * end.x; float y = inv * inv * current.y + 2 * inv * t * control.y + t * t * end.y; currentSegment.push_back({x, y}); } current = end; break; } case 'Z': case 'z': { // 闭合路径:将起点加入末尾 currentSegment.push_back(startPoint); segments.push_back(currentSegment); currentSegment.clear(); current = startPoint; break; } default: std::cerr << "不支持的命令: " << cmd << std::endl; break; } } if (!currentSegment.empty()) segments.push_back(currentSegment); return segments;} // 打印画布,将二维字符数组输出到控制台void printCanvas(const vector<vector<char>>& canvas) { for (size_t y = 0; y < canvas.size(); y++) { for (size_t x = 0; x < canvas[0].size(); x++) { cout << canvas[y][x]; } cout << endl; }} // 扫描线填充算法void scanlineFill(const vector<Point>& vertices, vector<vector<char>>& canvas, char fillChar) { int n = vertices.size(); if(n < 3) return; // 多边形至少有 3 个顶点 // 找出多边形顶点的最小和最大 y 值 int yMin = canvas.size(); int yMax = 0; for (const auto &pt : vertices) { if(pt.y < yMin) yMin = pt.y; if(pt.y > yMax) yMax = pt.y; } // 边数据结构,记录当前扫描需要的数据 struct EdgeRec { double x; // 当前扫描线交点 x 坐标 int yMax; // 边的最大 y 值(扫描到此值时,边退出活动边表) double invSlope; // 逆斜率 dx/dy int yMin; // 边的最小 y 值(开始参与扫描填充的 y 值) }; // 构建边表:遍历所有多边形边,跳过水平边 vector<EdgeRec> edgeTable; for (int i = 0; i < n; i++){ Point cur = vertices[i]; Point nxt = vertices[(i + 1) % n]; // 如果为水平边,则跳过 if (cur.y == nxt.y) continue; EdgeRec edge; if(cur.y < nxt.y) { edge.yMin = cur.y; edge.yMax = nxt.y; edge.x = cur.x; edge.invSlope = double(nxt.x - cur.x) / double(nxt.y - cur.y); } else { edge.yMin = nxt.y; edge.yMax = cur.y; edge.x = nxt.x; edge.invSlope = double(cur.x - nxt.x) / double(cur.y - nxt.y); } edgeTable.push_back(edge); } // 活动边表,存储当前扫描线与多边形相交的边 vector<EdgeRec> activeEdges; // 从 ymin 扫描到 ymax - 1(如果想要严格按像素填充,也可以扫描到 yMax) for (int y = yMin; y < yMax; y++) { // 将从当前扫描线开始的新边加入 activeEdges for (const auto &e : edgeTable) { if(e.yMin == y) activeEdges.push_back(e); } // 删除那些已经结束的边(当前 y 已经达到或超过其 yMax) activeEdges.erase(remove_if(activeEdges.begin(), activeEdges.end(), [y](const EdgeRec &e) { return y >= e.yMax; }), activeEdges.end()); // 对活动边表按交点 x 坐标排序 sort(activeEdges.begin(), activeEdges.end(), [](const EdgeRec &a, const EdgeRec &b){ return a.x < b.x; }); // 两两配对填充扫描线内的像素,假设活动边表中的边数为偶数 for (size_t i = 0; i < activeEdges.size(); i += 2) { if(i + 1 < activeEdges.size()) { int xStart = int(ceil(activeEdges[i].x)); int xEnd = int(floor(activeEdges[i + 1].x)); for (int x = xStart; x <= xEnd; x++){ // 检查边界后填充 if(y >= 0 && y < (int)canvas.size() && x >= 0 && x < (int)canvas[0].size()) canvas[y][x] = fillChar; } } } // 更新活动边表中所有边的 x 坐标,进入下一扫描线 for(auto &edge : activeEdges) { edge.x += edge.invSlope; } }} int main() { // 定义画布宽度和高度(例如 80x60 的字符画布) int width = 80; int height = 60; // 用'.'填充画布背景 vector<vector<char>> canvas(height, vector<char>(width, '.')); string pathData = "M801.99 734.51 l26.55 26.55 q-97.35 112.83 -219.03 175.88 q-121.68 63.06 -238.93 63.06 q-130.54 0 -211.29 -84.07 q-80.75 -84.07 -80.75 -219.03 q0 -161.5 99.55 -325.22 q99.55 -163.72 255.53 -267.7 q155.98 -103.98 310.85 -103.98 q81.86 0 129.42 39.83 q47.57 39.83 47.57 110.61 q0 143.81 -159.29 256.64 q-159.29 112.83 -407.08 148.23 l-61.95 8.85 q-22.12 88.49 -22.12 137.17 q0 90.71 56.42 144.91 q56.42 54.2 151.55 54.2 q70.8 0 137.17 -34.29 q66.37 -34.29 185.84 -131.64 ZM308.63 515.49 q119.47 -28.76 209.07 -70.8 q89.6 -42.04 137.16 -81.86 q47.56 -39.82 76.33 -86.28 q28.77 -46.46 36.51 -77.44 q7.74 -30.98 7.74 -61.95 q0 -37.61 -21.02 -61.95 q-21.01 -24.34 -56.41 -24.34 q-84.07 0 -190.26 109.52 q-106.19 109.52 -161.5 257.75 l-37.61 97.35 Z"; std::vector<PathSegment> segments = parseSVGPath(pathData); if (segments.empty()) { std::cerr << "未解析出路径数据!" << std::endl; return -1; } float offsetX = 0.0f; float offsetY = 0.0f; vector<Point> polygon; // 添加三角形 for (const auto &segment : segments) { for (size_t i = 0; i < segment.size() - 1; i++) { // 添加点到 polygon polygon.push_back({(segment[i].x + offsetX)/20, (segment[i].y + offsetY)/20}); } } // 使用扫描线填充算法填充多边形,填充字符为'#' scanlineFill(polygon, canvas, '*'); // 打印填充后的画布 printCanvas(canvas); return 0;} 结果输出:
................................***********.................................................................****************..............................................................*******....********............................................................******.......********.........................................................******..........*******.......................................................*******...........*******......................................................*******............********...................................................********.............********...................................................*******..............*******..................................................********...............*******..................................................*******................*******................................................*********...............********................................................********................********...............................................********................********...............................................*********...............********................................................********................********..............................................**********...............********...............................................*********..............*********...............................................**********.............*********................................................*********.............********.................................................**********...........*********..................................................*********..........*********...................................................**********........*********.....................................................**********.....**********.......................................................*********...************.......................................................*********************...........................................................*****************..............................................................*************...................................................................**********......................................................................**********......................................................................**********......................................................................*********.......................................................................*********......................................................................**********......................................................................**********......................................................................**********.......................................................................*********.......................................................................*********.........................***...........................................**********.......................***............................................**********......................***.............................................***********...................****...............................................**********..................****................................................***********...............*****..................................................************...........******....................................................****************..*********.....................................................*************************.........................................................*********************............................................................******************................................................................*************........................................................................**............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................💭 原来这些都是计算机图形学最基础的知识。
在调试两遍之后,即便借助 AI 解释,源码很多地方还是不理解,不得不停下来补充了一些字体基础知识,好在 FreeType 官网(freetype.org)提供了相关信息。

调试的核心流程是:加载字体后解析出字符 'e' 的轮廓点数据,对这些轮廓点进行位移、缩放等变换,得到用于光栅化的坐标数据,最后将这些坐标数据光栅化(Rasterize)生成位图(Bitmap)。
由于环节较多,为方便记忆流程(避免回溯)并重点关注光栅化过程,绘制一个时序图将很有帮助。
字体渲染时序图

以下是 FreeType 加载 TrueType 字体时涉及的核心类功能概括:
| 类/模块 | 关键技术点 |
|---|---|
| ftobjs | • 管理 FT_Library 和 FT_Face 对象生命周期• 处理字体引擎初始化/销毁• 协调各模块间通信 |
| ttdriver | • 解析 TTF/OTF 字体文件结构• 解释 glyf/loca 等关键表• 处理 hinting 指令 |
| ftgloadr | • 从字体文件加载字形轮廓数据• 处理复合字形组件• 转换坐标到设备空间 |
| ftsmooth | • 灰度抗锯齿渲染• LCD 子像素渲染• Gamma 校正处理 |
| ftoutln | • 解析 TrueType 轮廓指令• 轮廓变换(旋转/缩放)• 轮廓点遍历与优化 |
| ftgrays | • 扫线算法实现轮廓光栅化• 生成抗锯齿位图• 8位/16位灰度处理 |
💭 表格和下面流程都是通过 copilot 代码解释获得。调试中,Copilot 提供的代码解释大多似是而非(或:缺乏细节)。它通常只分析当前函数的直接上下文,难以理解 A→B→C 这类嵌套调用的完整逻辑。不确定切换到 Cursor 是否能提供真正的项目级上下文理解。
主要流程说明
- ft_open_face_internal()
用于打开一个字体文件并创建 FT_Face 对象,它主要进行参数预处理、流的创建、驱动程序选择、初始化、特殊场景处理等操作。
- tt_size_request()
用于处理字体尺寸的设置请求,主要处理两类字体:
- 嵌入式点阵字体:使用预先生成的位图
- 可缩放轮廓字体:使用数学轮廓进行缩放渲染(本示例)
- FT_Request_Metrics()
于根据不同的尺寸请求类型计算字体的尺寸度量(metrics)。这是字体渲染过程中的核心计算函数,负责将抽象尺寸请求转换为具体的缩放比例和像素尺寸。
- ft_recompute_scaled_metrics()
根据缩放比例重新计算字体的度量值(metrics)。这些度量值对于文本布局和渲染至关重要,包括上升高度(ascender)、下降高度(descender)、行高(height)和最大步进(max_advance)。确保在各种分辨率和缩放比例下都能获得清晰、一致的文本渲染效果。支持动态调整,无需重新加载字体文件。
FT_Request_Size (truetype driver): x scale: 201327 (3.072006) // x 轴缩放比例 y scale: 201327 (3.072006) // y 轴缩放比例 ascender: 44.000000 // 从水平基线到字体面上最高“字符”坐标的垂直距离。 descender: -15.000000 // 下从水平基线到字体面中最低“字符”坐标的垂直距离。 height: 62.000000 // 此字段代表使用该字体编写文本时的默认线间距(即基线到基线距离)。请注意,它通常大于将其作为绝对值的升华和下降的总和。 max advance: 77.000000 // 该字段为字体中所有字形提供了最大的水平光标步进。 x ppem: 48 // X 轴的点阵大小 y ppem: 48 // Y 轴的点阵大小 // 准确释义:https://freetype.org/freetype2/docs/tutorial/step2.html#section-2- tt_glyph_load()
加载 TrueType 格式的字形数据到字形槽(glyph slot)中
- slot: 字形槽指针,用于存储加载的字形
- size: 字体大小对象
- glyph_index: 要加载的字形索引
- load_flags: 加载标志位,控制加载行为
- TT_Load_Glyph()
负责从字体文件中加载指定索引的字形数据。
- load_truetype_glyph()
从TrueType字体文件中加载单个字形数据。
- TT_Load_Simple_Glyph()
中TrueType字体引擎的核心部分,用于加载简单字形(非复合字形)的轮廓数据 解析TrueType字体文件中的简单字形轮廓数据,包括:
- 轮廓结构(contours)
- 点坐标(points)
- 点标志(tags)
- 字形指令(glyph instructions/bytecode)
// 解析字体文件读取的原始轮廓点数据======>[read points] Point 0: x=358.000000, y=109.000000======>[read points] Point 1: x=370.000000, y=97.000000======>[read points] Point 2: x=326.000000, y=46.000000======>[read points] Point 3: x=216.000000, y=-11.000000======>[read points] Point 4: x=163.000000, y=-11.000000======>[read points] Point 5: x=104.000000, y=-11.000000======>[read points] Point 6: x=31.000000, y=65.000000======>[read points] Point 7: x=31.000000, y=126.000000======>[read points] Point 8: x=31.000000, y=199.000000 ======>[read tags] Tag 0: 1 // 1 在轮廓线上的点======>[read tags] Tag 1: 1======>[read tags] Tag 2: 0 // 0 二次贝塞尔曲线的控制点======>[read tags] Tag 3: 0======>[read tags] Tag 4: 1======>[read tags] Tag 5: 0======>[read tags] Tag 6: 0======>[read tags] Tag 7: 1======>[read tags] Tag 8: 0- TT_Process_Simple_Glyph()
负责处理 TrueType 简单字形(非复合字形)的最终渲染流程,主要完成:
- 幻影点设置:添加四个关键定位点(pp1-pp4)到字形轮廓
- 可变字体支持:应用 GX 变形表的字形坐标调整
- 轮廓点缩放:将设计单位转换为设备像素单位
- 字形提示处理:执行字节码提示优化小字号显示效果
// 缩放后======>[after scale] Point 0: x=1100.000000, y=335.000000======>[after scale] Point 1: x=1137.000000, y=298.000000======>[after scale] Point 2: x=1001.000000, y=141.000000======>[after scale] Point 3: x=664.000000, y=-34.000000======>[after scale] Point 4: x=501.000000, y=-34.000000======>[after scale] Point 5: x=319.000000, y=-34.000000======>[after scale] Point 6: x=95.000000, y=200.000000======>[after scale] Point 7: x=95.000000, y=387.000000======>[after scale] Point 8: x=95.000000, y=611.000000- compute_glyph_metrics()
该函数负责计算字形的各种度量值,包括:
- 水平度量:水平起始位置(horiBearingX/Y)、水平步进值(horiAdvance)
- 垂直度量:垂直起始位置(vertBearingX/Y)、垂直步进值(vertAdvance)
- 字形尺寸:宽度(width)和高度(height)
- 线性步进值:未经缩放的水平和垂直步进值(linearHoriAdvance/linearVertAdvance)
这些度量值对文本布局至关重要,决定了字符间的间距和对齐方式。
- ft_smooth_render()
FreeType 库中实现抗锯齿字形渲染的核心函数,负责将矢量轮廓(outline)转换为平滑的位图(bitmap) 该函数是 FreeType 渲染器的核心实现,主要完成以下任务:
- 参数校验:检查字形格式与渲染模式是否兼容
- 位图内存管理:释放旧位图并分配新缓冲区
- 坐标变换:根据渲染模式调整轮廓位置
- 光栅化处理:根据不同模式调用抗锯齿或 LCD 子像素渲染
- 后处理:应用 LCD 滤波优化显示效果
- ft_glyphslot_preset_bitmap()
是 FreeType 中预计算字形位图参数的核心函数,用于根据轮廓和渲染模式确定位图尺寸、位置和内存布局。该函数为字形槽(glyph slot)预设位图参数,包括:
- 位图尺寸计算:基于轮廓控制框(control box)
- 像素模式选择:根据渲染模式确定像素格式
- 内存布局规划:计算行跨度(pitch)和内存对齐
- 坐标转换:将轮廓坐标转换为位图物理坐标
======>[bitmap size] width:19,height:23- FT_Outline_Translate
对缩放后的点进行位移操作
// 位移后======>[after translate] Point 0: x=1036.000000, y=399.000000======>[after translate] Point 1: x=1073.000000, y=362.000000======>[after translate] Point 2: x=937.000000, y=205.000000======>[after translate] Point 3: x=600.000000, y=30.000000======>[after translate] Point 4: x=437.000000, y=30.000000======>[after translate] Point 5: x=255.000000, y=30.000000======>[after translate] Point 6: x=31.000000, y=264.000000======>[after translate] Point 7: x=31.000000, y=451.000000======>[after tranlate] Point 8: x=31.000000, y=675.000000- gray_convert_glyph()
该函数是 FreeType 中用于灰度抗锯齿字形光栅化的核心分带渲染算法,此函数实现分带式光栅化,通过垂直分带 + 水平二分策略高效处理大字形,避免内存溢出。核心流程分为:
- 垂直分带:将字形高度分割为多个垂直带
- 水平分带:对每个垂直带进行水平二分递归
- 单元格渲染:在子带内生成覆盖数据
- 像素填充:将单元格数据转换为位图像素
ras.ycells = (PCell*)buffer; // 存储每行的单元格链表头指针,数组+链表的数据结构- FT_Outline_Decompose()
库中 FT_Outline_Decompose 函数的详细解析,该函数是矢量字形轮廓分解的核心实现:
- 轮廓分解:将复杂的轮廓路径分解为基本图元(直线、二次/三次贝塞尔曲线)
- 坐标转换:应用缩放和偏移变换(通过 shift 和 delta 参数)
- 回调驱动:通过函数指针调用用户定义的绘图操作
- 轮廓闭合:确保每个轮廓路径正确闭合
// 把 26.6 定点数转化成 float 类型,这些便是用于光栅化的坐标数据======>[preparing to convert to bitmap] 0: x=16.187500, y=6.234375======>[preparing to convert to bitmap] 1: x=16.765625, y=5.656250======>[preparing to convert to bitmap] 2: x=14.640625, y=3.203125======>[preparing to convert to bitmap] 3: x=9.375000, y=0.468750======>[preparing to convert to bitmap] 4: x=6.828125, y=0.468750======>[preparing to convert to bitmap] 5: x=3.984375, y=0.468750======>[preparing to convert to bitmap] 6: x=0.484375, y=4.125000======>[preparing to convert to bitmap] 7: x=0.484375, y=7.046875======>[preparing to convert to bitmap] 8: x=0.484375, y=10.546875这个函数里面调度 gray_render_line(),二次贝塞尔曲线也会被切分成直线,线经过的像素点都被计算 cover、area 值。



💡 fig.17、fig.18 输出用于光栅化的坐标数据到 Mathemathic,然后使用 Figma 把灰度图与 Mathemathic 导出图片进行叠加。
- gray_sweep()
库中 gray_sweep 函数的详细解析,该函数是灰度抗锯齿渲染的核心实现,负责将单元格数据转换为最终位图像素:
- 抗锯齿计算:将单元格覆盖率转换为灰度值
- 像素填充:根据奇偶填充规则处理轮廓
- 位图写入:将计算结果写入目标位图缓冲区
- 边界处理:处理裁剪区域边缘情况
总结
这篇博客记录了我在学习过程中大量使用 AI 工具的经历,导致结构略显凌乱,AI 的回答与我的叙述之间也不可避免地存在一些割裂感。但无论如何,这都是一份真实的学习记录,就让它在此告一段落吧。
即使有 AI 工具的辅助,如果从未亲自深入钻研代码和原理,我们也很难建立起真正的理解。然而,AI 工具确实能帮助我们更快地达成目标。
关键在于,AI 并不会取代你的编程能力,它更像是为你的引擎注入了一剂强效氮气,带来加速的力量。它与 Figma、Mathematica 等工具一样,本质上是一种拓展能力的杠杆。你掌握的工具越多,能进行的尝试就越丰富。这些工具节省下来的时间,让你能够更系统地解决问题、有更多的时间创造出更出色的作品。
总之,切勿让人工智能代替你思考,因为编程的核心,永远在于思考本身。


