前言
“文字是如何渲染的?” 这个看似简单的问题,往往被我们忽略。每天,我们与屏幕上的文字打交道,输入、阅读,流畅得如同呼吸。然而,很少有人停下来思考:屏幕上清晰呈现的每一个字符背后,究竟隐藏着怎样精密而复杂的技术链条?
这篇博客并非要深入探讨晦涩难懂的专业知识,而更像是一份探索笔记,忠实记录我试图解开这一“简单”谜题的思考轨迹、所学知识,以及意外发现的有趣联系。希望与你一同踏上这段从“习以为常”到“恍然大悟”的旅程。
开发环境
1Mac M1 2 3VSCode + CMake4 5OpenGL6 7AI 工具:DeepSeek、Copilot、Gemini8 9其它工具:Figma 、geogebra 科学计算器、Wolfram Mathematica10 11字体:FreeSerifItalic.ttf、AlibabaPuHuiTi-3-55-Regular.ttf文字渲染
先来问问 AI,看能不能直接得到满意的答案。
文字是怎么渲染的?
文字是怎么渲染的?
文字渲染是一个复杂但 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 原理
💭 TrueType 这个数字化字体标准的诞生,是否是乔布斯在斯坦福大学演讲中提到的退学后参加书法课直到后来应用到个人电脑中的现实写照?
效仿 Easy Scalable Text Rendering on the GPU 博客,使用 FreeSerifItalic.ttf 字体中的字符 ‘e’ 作为示例,可以从字体在线编辑软件导出字符 e.svg 文件。
导出的 e.svg 文件其实是一个 XML 文件。
1<?xml version="1.0" encoding="utf-8"?>2<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">3<svg id="e" width="128" height="128" style="width:128px;height:128px;" version="1.1"4 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">5 <path fill="#666" d="M801.99 734.51 6 l26.55 26.55 7 q-97.35 112.83 -219.03 175.88 8 q-121.68 63.06 -238.93 63.06 9 q-130.54 0 -211.29 -84.07 10 q-80.75 -84.07 -80.75 -219.03 11 q0 -161.5 99.55 -325.22 12 q99.55 -163.72 255.53 -267.7 13 q155.98 -103.98 310.85 -103.98 14 q81.86 0 129.42 39.83 15 q47.57 39.83 47.57 110.61 16 q0 143.81 -159.29 256.64 17 q-159.29 112.83 -407.08 148.23 18 l-61.95 8.85 19 q-22.12 88.49 -22.12 137.17 20 q0 90.71 56.42 144.91 21 q56.42 54.2 151.55 54.2 22 q70.8 0 137.17 -34.29 23 q66.37 -34.29 185.84 -131.64 24 ZM308.63 515.49 25 q119.47 -28.76 209.07 -70.8 26 q89.6 -42.04 137.16 -81.86 27 q47.56 -39.82 76.33 -86.28 28 q28.77 -46.46 36.51 -77.44 29 q7.74 -30.98 7.74 -61.95 30 q0 -37.61 -21.02 -61.95 31 q-21.01 -24.34 -56.41 -24.34 32 q-84.07 0 -190.26 109.52 33 q-106.19 109.52 -161.5 257.75 34 l-37.61 97.35 Z"/>35</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
1#include <iostream>2#include <math.h>3#define NANOSVG_IMPLEMENTATION // 启用 NanoSVG 实现4#include "nanosvg/src/nanosvg.h"5 6#include <GLFW/glfw3.h>7 8 9/**10 * 思路:11 * 1. 使用 nanosvg 解析 svg 文件12 * 2. 绘制出对应的路径、锚点和控制点13 */14NSVGimage* g_image = NULL;15 16static unsigned char bgColor[4] = {247,247,246,255};17static unsigned char lineColor[4] = {0,0,255,255};18 19static float distPtSeg(float x, float y, float px, float py, float qx, float qy)20{21 float pqx, pqy, dx, dy, d, t;22 pqx = qx-px;23 pqy = qy-py;24 dx = x-px;25 dy = y-py;26 d = pqx*pqx + pqy*pqy;27 t = pqx*dx + pqy*dy;28 if (d > 0) t /= d;29 if (t < 0) t = 0;30 else if (t > 1) t = 1;31 dx = px + t*pqx - x;32 dy = py + t*pqy - y;33 return dx*dx + dy*dy;34}35 36static void cubicBez(float x1, float y1, float x2, float y2,37 float x3, float y3, float x4, float y4,38 float tol, int level)39{40 float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234;41 float d;42 43 if (level > 12) return;44 45 x12 = (x1+x2)*0.5f;46 y12 = (y1+y2)*0.5f;47 x23 = (x2+x3)*0.5f;48 y23 = (y2+y3)*0.5f;49 x34 = (x3+x4)*0.5f;50 y34 = (y3+y4)*0.5f;51 x123 = (x12+x23)*0.5f;52 y123 = (y12+y23)*0.5f;53 x234 = (x23+x34)*0.5f;54 y234 = (y23+y34)*0.5f;55 x1234 = (x123+x234)*0.5f;56 y1234 = (y123+y234)*0.5f;57 58 d = distPtSeg(x1234, y1234, x1,y1, x4,y4);59 if (d > tol*tol) {60 cubicBez(x1,y1, x12,y12, x123,y123, x1234,y1234, tol, level+1); 61 cubicBez(x1234,y1234, x234,y234, x34,y34, x4,y4, tol, level+1); 62 } else {63 glVertex2f(x4, y4);64 }65}66 67void drawPath(float* pts, int npts, char closed, float tol)68{69 int i;70 glBegin(GL_LINE_STRIP);71 glColor4ubv(lineColor);72 glVertex2f(pts[0], pts[1]);73 for (i = 0; i < npts-1; i += 3) {74 float* p = &pts[i*2];75 cubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7], tol, 0);76 }77 if (closed) {78 glVertex2f(pts[0], pts[1]);79 }80 glEnd();81}82 83void drawControlPts(float* pts, int npts)84{85 int i;86 87 // Control lines88 glColor4ubv(lineColor);89 glBegin(GL_LINES);90 for (i = 0; i < npts-1; i += 3) {91 float* p = &pts[i*2];92 glVertex2f(p[0],p[1]);93 glVertex2f(p[2],p[3]);94 glVertex2f(p[4],p[5]);95 glVertex2f(p[6],p[7]);96 }97 glEnd();98 99 // Points100 glPointSize(6.0f);101 glColor4ubv(lineColor);102 103 glBegin(GL_POINTS);104 glVertex2f(pts[0],pts[1]);105 for (i = 0; i < npts-1; i += 3) {106 float* p = &pts[i*2];107 glVertex2f(p[6],p[7]);108 }109 glEnd();110 111 // Points112 glPointSize(3.0f);113 114 glBegin(GL_POINTS);115 glColor4ubv(bgColor);116 glVertex2f(pts[0],pts[1]);117 for (i = 0; i < npts-1; i += 3) {118 float* p = &pts[i*2];119 glColor4ubv(lineColor);120 glVertex2f(p[2],p[3]);121 glVertex2f(p[4],p[5]);122 glColor4ubv(bgColor);123 glVertex2f(p[6],p[7]);124 }125 glEnd();126}127 128void drawframe(GLFWwindow* window)129{130 int width = 0, height = 0;131 float view[4], cx, cy, hw, hh, aspect, px;132 NSVGshape* shape;133 NSVGpath* path;134 135 glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);136 glfwGetFramebufferSize(window, &width, &height);137 138 glViewport(0, 0, width, height);139 glClearColor(0.974f, 0.974f, 0.978f, 1.000f);140 glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);141 glEnable(GL_BLEND);142 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);143 glDisable(GL_TEXTURE_2D);144 glMatrixMode(GL_PROJECTION);145 glLoadIdentity();146 147 // Fit view to bounds148 cx = g_image->width*0.5f;149 cy = g_image->height*0.5f;150 hw = g_image->width*0.5f;151 hh = g_image->height*0.5f;152 153 if (width/hw < height/hh) {154 aspect = (float)height / (float)width;155 view[0] = cx - hw * 1.2f;156 view[2] = cx + hw * 1.2f;157 view[1] = cy - hw * 1.2f * aspect;158 view[3] = cy + hw * 1.2f * aspect;159 } else {160 aspect = (float)width / (float)height;161 view[0] = cx - hh * 1.2f * aspect;162 view[2] = cx + hh * 1.2f * aspect;163 view[1] = cy - hh * 1.2f;164 view[3] = cy + hh * 1.2f;165 }166 // Size of one pixel.167 px = (view[2] - view[1]) / (float)width;168 169 glOrtho(view[0], view[2], view[3], view[1], -1, 1);170 171 glMatrixMode(GL_MODELVIEW);172 glLoadIdentity();173 glDisable(GL_DEPTH_TEST);174 glColor4ub(255,255,255,255);175 glEnable(GL_BLEND);176 glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);177 178 // Draw bounds179 glColor4ub(0,0,0,64);180 glBegin(GL_LINE_LOOP);181 glVertex2f(0, 0);182 glVertex2f(g_image->width, 0);183 glVertex2f(g_image->width, g_image->height);184 glVertex2f(0, g_image->height);185 glEnd();186 187 for (shape = g_image->shapes; shape != NULL; shape = shape->next) {188 for (path = shape->paths; path != NULL; path = path->next) {189 drawPath(path->pts, path->npts, path->closed, px * 1.5f);190 drawControlPts(path->pts, path->npts);191 }192 }193 194 glfwSwapBuffers(window);195}196 197void resizecb(GLFWwindow* window, int width, int height)198{199 // Update and render200 NSVG_NOTUSED(width);201 NSVG_NOTUSED(height);202 drawframe(window);203}204 205int main()206{207 GLFWwindow* window;208 const GLFWvidmode* mode;209 210 if (!glfwInit())211 return -1;212 213 mode = glfwGetVideoMode(glfwGetPrimaryMonitor());214 window = glfwCreateWindow(mode->width - 40, mode->height - 80, "Nano SVG", NULL, NULL);215 if (!window)216 {217 printf("Could not open window\n");218 glfwTerminate();219 return -1;220 }221 222 glfwSetFramebufferSizeCallback(window, resizecb);223 glfwMakeContextCurrent(window);224 glEnable(GL_POINT_SMOOTH);225 glEnable(GL_LINE_SMOOTH);226 227 228 g_image = nsvgParseFromFile("e.svg", "px", 96.0f);229 if (g_image == NULL) {230 printf("Could not open SVG image.\n");231 glfwTerminate();232 return -1;233 }234 235 while (!glfwWindowShouldClose(window))236 {237 drawframe(window);238 glfwPollEvents();239 }240 241 nsvgDelete(g_image);242 243 glfwTerminate();244 return 0;245}246 添加 stb_image_write.h,即可把 SVG 文件转换为图片。
💭 svg 转 png 很简单,反之要复杂很多。
示例代码:SVG 文件解析并转为 png 图片
1#include <stdio.h>2#include <string.h>3#include <float.h>4#define STB_IMAGE_WRITE_IMPLEMENTATION5#include "stb_image_write.h"6#define NANOSVG_IMPLEMENTATION7#include "nanosvg.h"8#define NANOSVGRAST_IMPLEMENTATION9#include "nanosvgrast.h"10 11/**12 * 1. 使用 nanosvg 解析 svg 文件13 * 2. 导出为图片14 */15int main()16{17 NSVGimage *image = NULL;18 NSVGrasterizer *rast = NULL;19 unsigned char *img = NULL;20 int w, h;21 const char *filename = "e.svg";22 23 printf("parsing %s\n", filename);24 image = nsvgParseFromFile(filename, "px", 48.0f);25 if (image == NULL)26 {27 printf("Could not open SVG image.\n");28 goto error;29 }30 w = (int)image->width;31 h = (int)image->height;32 33 rast = nsvgCreateRasterizer();34 if (rast == NULL)35 {36 printf("Could not init rasterizer.\n");37 goto error;38 }39 40 img = (unsigned char *)malloc(w * h * 4);41 if (img == NULL)42 {43 printf("Could not alloc image buffer.\n");44 goto error;45 }46 47 printf("rasterizing image %d x %d\n", w, h);48 nsvgRasterize(rast, image, 0, 0, 1, img, w, h, w * 4);49 50 printf("writing export_e.png\n");51 stbi_write_png("export_e.png", w, h, 4, img, w * 4);52 53error:54 nsvgDeleteRasterizer(rast);55 nsvgDelete(image);56 57 return 0;58}59 手动解析 e.svg
基于上述示例,我们已了解 SVG 的基本概念。值得注意的是,NanoSVG 会将路径数据解析为包含三次贝塞尔曲线的结构(与 TrueType 使用的二次曲线不同,NanoSVG 通过插点把二次贝塞尔曲线转为三次贝塞尔曲线),因此我们可直接解析 e.svg 文件中的路径信息。
示例代码:解析 e.svg 中的点,并绘制这些点
1#include <iostream>2#include <math.h>3#include <vector>4#include <string>5#include <cctype>6#include <iomanip>7 8#include <glad/glad.h>9#include <GLFW/glfw3.h>10 11/**12 * 思路:13 * 1. 解析 svg 文件中的点14 * 2. 绘制这些点15 */16using namespace std;17 18struct Point19{20 double x;21 double y;22};23 24vector<string> tokenize(const string &path)25{26 vector<string> tokens;27 string token;28 bool readingNumber = false;29 30 for (char c : path)31 {32 if (isdigit(c) || c == '.' || c == '-' || c == '+')33 {34 readingNumber = true;35 token += c;36 }37 else38 {39 if (readingNumber)40 {41 tokens.push_back(token);42 token.clear();43 readingNumber = false;44 }45 if (isalpha(c) || c == ',')46 {47 if (c != ',')48 { // 忽略逗号49 tokens.push_back(string(1, c));50 }51 }52 }53 }54 55 if (!token.empty())56 {57 tokens.push_back(token);58 }59 60 return tokens;61}62 63vector<Point> parseSVGPath(const string &pathData)64{65 vector<Point> points;66 vector<string> tokens = tokenize(pathData);67 68 double currentX = 0.0;69 double currentY = 0.0;70 size_t i = 0;71 72 while (i < tokens.size())73 {74 string cmd = tokens[i++];75 76 if (cmd == "M" || cmd == "m")77 { // MoveTo78 double x = stod(tokens[i++]);79 double y = stod(tokens[i++]);80 81 if (cmd == "m")82 {83 currentX += x;84 currentY += y;85 }86 else87 {88 currentX = x;89 currentY = y;90 }91 points.push_back({currentX, currentY});92 }93 else if (cmd == "L" || cmd == "l")94 { // LineTo95 double x = stod(tokens[i++]);96 double y = stod(tokens[i++]);97 98 if (cmd == "l")99 {100 currentX += x;101 currentY += y;102 }103 else104 {105 currentX = x;106 currentY = y;107 }108 points.push_back({currentX, currentY});109 }110 else if (cmd == "Q" || cmd == "q")111 { // Quadratic Bezier112 // 跳过控制点(前两个参数)113 i += 2;114 double x = stod(tokens[i++]);115 double y = stod(tokens[i++]);116 117 if (cmd == "q")118 {119 currentX += x;120 currentY += y;121 }122 else123 {124 currentX = x;125 currentY = y;126 }127 points.push_back({currentX, currentY});128 }129 else if (cmd == "Z" || cmd == "z")130 { // ClosePath131 // 不需要处理坐标132 }133 else134 {135 cerr << "Unsupported command: " << cmd << endl;136 break;137 }138 }139 140 return points;141}142 143// 顶点着色器144const char *vertexShaderSource = R"(145 #version 330 core146 layout(location = 0) in vec2 aPos;147 void main() {148 gl_Position = vec4(aPos.x, -aPos.y, 0.0, 1.0);149 }150 )";151 152// 片段着色器153const char *fragmentShaderSource = R"(154 #version 330 core155 out vec4 FragColor;156 void main() {157 FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色点158 }159 )";160 161// 编译着色器162GLuint compileShader(GLenum type, const char *source)163{164 GLuint shader = glCreateShader(type);165 glShaderSource(shader, 1, &source, nullptr);166 glCompileShader(shader);167 168 GLint success;169 glGetShaderiv(shader, GL_COMPILE_STATUS, &success);170 if (!success)171 {172 char infoLog[512];173 glGetShaderInfoLog(shader, 512, nullptr, infoLog);174 std::cerr << "ERROR: SHADER COMPILATION\n"175 << infoLog << std::endl;176 }177 return shader;178}179 180// 创建着色器程序181GLuint createShaderProgram()182{183 GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);184 GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);185 186 GLuint shaderProgram = glCreateProgram();187 glAttachShader(shaderProgram, vertexShader);188 glAttachShader(shaderProgram, fragmentShader);189 glLinkProgram(shaderProgram);190 191 glDeleteShader(vertexShader);192 glDeleteShader(fragmentShader);193 return shaderProgram;194}195 196int main()197{198 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";199 200 vector<Point> tempPoints = parseSVGPath(pathData);201 202 cout << fixed << setprecision(2);203 cout << "Extracted Points (excluding Q/q control points):\n";204 for (const auto &p : tempPoints)205 {206 cout << "(" << p.x << ", " << p.y << ")\n";207 }208 209 if (!glfwInit())210 {211 std::cerr << "无法初始化 GLFW" << std::endl;212 return -1;213 }214 215 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);216 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);217 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);218 219 GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGL Render e.svg Points", nullptr, nullptr);220 if (!window)221 {222 std::cerr << "无法创建窗口" << std::endl;223 glfwTerminate();224 return -1;225 }226 227 glfwMakeContextCurrent(window);228 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))229 {230 std::cerr << "无法初始化 GLAD" << std::endl;231 glfwTerminate();232 return -1;233 }234 235 GLuint shaderProgram = createShaderProgram();236 237 std::vector<float> points;238 float offsetX = -0.5;239 float offsetY = -3.0;240 for (size_t i = 0; i < tempPoints.size(); ++i)241 {242 if (i == tempPoints.size() - 12)243 {244 continue;245 }246 247 points.push_back(tempPoints[i].x / 1000 + offsetX);248 points.push_back(tempPoints[i].y / 1000 + offsetX);249 }250 251 GLuint VAO, VBO;252 glGenVertexArrays(1, &VAO);253 glGenBuffers(1, &VBO);254 255 glBindVertexArray(VAO);256 glBindBuffer(GL_ARRAY_BUFFER, VBO);257 glBufferData(GL_ARRAY_BUFFER, points.size() * sizeof(float), points.data(), GL_STATIC_DRAW);258 259 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0);260 glEnableVertexAttribArray(0);261 262 glPointSize(6.0f);263 264 while (!glfwWindowShouldClose(window))265 {266 glClearColor(1.0f, 1.0f, 1.0f, 1.0f);267 glClear(GL_COLOR_BUFFER_BIT);268 269 glUseProgram(shaderProgram);270 glBindVertexArray(VAO);271 glDrawArrays(GL_POINTS, 0, points.size() / 2);272 273 glfwSwapBuffers(window);274 glfwPollEvents();275 }276 277 glDeleteVertexArrays(1, &VAO);278 glDeleteBuffers(1, &VBO);279 glDeleteProgram(shaderProgram);280 281 glfwDestroyWindow(window);282 glfwTerminate();283 return 0;284}💡 为了能看懂并修改 AI 工具生成的代码,使其运行通过,欢迎来到 OpenGL 的世界。
绘制这些点连接起来之后的线。
示例代码:解析 e.svg 文件并绘制直线
1#include <iostream>2#include <math.h>3#include <vector>4#include <string>5#include <cctype>6#include <iomanip>7 8#include <glad/glad.h>9#include <GLFW/glfw3.h>10 11 12/**13 * 思路:14 * 1. 解析 svg 文件中的点15 * 2. 相邻两点之间画线16 * 问题:17 * svg path 中 m 标识跳过,两条闭合曲线18 */19using namespace std;20 21struct Point22{23 double x;24 double y;25};26 27vector<string> tokenize(const string &path)28{29 vector<string> tokens;30 string token;31 bool readingNumber = false;32 33 for (char c : path)34 {35 if (isdigit(c) || c == '.' || c == '-' || c == '+')36 {37 readingNumber = true;38 token += c;39 }40 else41 {42 if (readingNumber)43 {44 tokens.push_back(token);45 token.clear();46 readingNumber = false;47 }48 if (isalpha(c) || c == ',')49 {50 if (c != ',')51 { // 忽略逗号52 tokens.push_back(string(1, c));53 }54 }55 }56 }57 58 if (!token.empty())59 {60 tokens.push_back(token);61 }62 63 return tokens;64}65 66vector<Point> parseSVGPath(const string &pathData)67{68 vector<Point> points;69 vector<string> tokens = tokenize(pathData);70 71 double currentX = 0.0;72 double currentY = 0.0;73 size_t i = 0;74 75 while (i < tokens.size())76 {77 string cmd = tokens[i++];78 79 if (cmd == "M" || cmd == "m")80 { // MoveTo81 double x = stod(tokens[i++]);82 double y = stod(tokens[i++]);83 84 if (cmd == "m")85 {86 currentX += x;87 currentY += y;88 }89 else90 {91 currentX = x;92 currentY = y;93 }94 points.push_back({currentX, currentY});95 }96 else if (cmd == "L" || cmd == "l")97 { // LineTo98 double x = stod(tokens[i++]);99 double y = stod(tokens[i++]);100 101 if (cmd == "l")102 {103 currentX += x;104 currentY += y;105 }106 else107 {108 currentX = x;109 currentY = y;110 }111 points.push_back({currentX, currentY});112 }113 else if (cmd == "Q" || cmd == "q")114 { // Quadratic Bezier115 // 跳过控制点(前两个参数)116 i += 2;117 double x = stod(tokens[i++]);118 double y = stod(tokens[i++]);119 120 if (cmd == "q")121 {122 currentX += x;123 currentY += y;124 }125 else126 {127 currentX = x;128 currentY = y;129 }130 points.push_back({currentX, currentY});131 }132 else if (cmd == "Z" || cmd == "z")133 { // ClosePath134 // 不需要处理坐标135 }136 else137 {138 cerr << "Unsupported command: " << cmd << endl;139 break;140 }141 }142 143 return points;144}145 146// 顶点着色器147const char *vertexShaderSource = R"(148 #version 330 core149 layout(location = 0) in vec2 aPos;150 void main() {151 gl_Position = vec4(aPos.x, -aPos.y, 0.0, 1.0);152 }153 )";154 155// 片段着色器156const char *fragmentShaderSource = R"(157 #version 330 core158 out vec4 FragColor;159 void main() {160 FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色点161 }162 )";163 164// 编译着色器165GLuint compileShader(GLenum type, const char *source)166{167 GLuint shader = glCreateShader(type);168 glShaderSource(shader, 1, &source, nullptr);169 glCompileShader(shader);170 171 GLint success;172 glGetShaderiv(shader, GL_COMPILE_STATUS, &success);173 if (!success)174 {175 char infoLog[512];176 glGetShaderInfoLog(shader, 512, nullptr, infoLog);177 std::cerr << "ERROR: SHADER COMPILATION\n"178 << infoLog << std::endl;179 }180 return shader;181}182 183// 创建着色器程序184GLuint createShaderProgram()185{186 GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);187 GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);188 189 GLuint shaderProgram = glCreateProgram();190 glAttachShader(shaderProgram, vertexShader);191 glAttachShader(shaderProgram, fragmentShader);192 glLinkProgram(shaderProgram);193 194 glDeleteShader(vertexShader);195 glDeleteShader(fragmentShader);196 return shaderProgram;197}198 199int main()200{201 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";202 203 vector<Point> tempPoints = parseSVGPath(pathData);204 205 cout << fixed << setprecision(2);206 cout << "Extracted Points (excluding Q/q control points):\n";207 for (const auto &p : tempPoints)208 {209 cout << "(" << p.x << ", " << p.y << ")\n";210 }211 212 if (!glfwInit())213 {214 std::cerr << "无法初始化 GLFW" << std::endl;215 return -1;216 }217 218 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);219 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);220 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);221 222 GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGL Render e.svg Lines", nullptr, nullptr);223 if (!window)224 {225 std::cerr << "无法创建窗口" << std::endl;226 glfwTerminate();227 return -1;228 }229 230 glfwMakeContextCurrent(window);231 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))232 {233 std::cerr << "无法初始化 GLAD" << std::endl;234 glfwTerminate();235 return -1;236 }237 238 GLuint shaderProgram = createShaderProgram();239 240 std::vector<float> lines;241 float offsetX = -0.5;242 float offsetY = -3.0;243 for (size_t i = 0; i < tempPoints.size()-1; ++i)244 {245 // 这里减掉 svg path 中 zm 那条线246 if (i == tempPoints.size() - 12)247 {248 continue;249 }250 251 lines.push_back(tempPoints[i].x / 1000 + offsetX);252 lines.push_back(tempPoints[i].y / 1000 + offsetX);253 lines.push_back(tempPoints[i+1].x / 1000 + offsetX);254 lines.push_back(tempPoints[i+1].y / 1000 + offsetX);255 }256 257 GLuint VAO, VBO;258 glGenVertexArrays(1, &VAO);259 glGenBuffers(1, &VBO);260 261 glBindVertexArray(VAO);262 glBindBuffer(GL_ARRAY_BUFFER, VBO);263 glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(float), lines.data(), GL_STATIC_DRAW);264 265 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0);266 glEnableVertexAttribArray(0);267 268 glPointSize(4.0f);269 270 while (!glfwWindowShouldClose(window))271 {272 glClearColor(0.974f, 0.974f, 0.978f, 1.000f);273 glClear(GL_COLOR_BUFFER_BIT);274 275 glUseProgram(shaderProgram);276 glBindVertexArray(VAO);277 glDrawArrays(GL_LINES, 0, lines.size() / 2);278 279 glDrawArrays(GL_POINTS, 0, lines.size() / 2);280 281 glfwSwapBuffers(window);282 glfwPollEvents();283 }284 285 glDeleteVertexArrays(1, &VAO);286 glDeleteBuffers(1, &VBO);287 glDeleteProgram(shaderProgram);288 289 glfwDestroyWindow(window);290 glfwTerminate();291 return 0;292}使用 Easy Scalable Text Rendering on the GPU 中的方式,选择一个参考点,与解析出的点(轮廓上的点)两两构成三角形。
示例代码:选择一个参考点,遍历解析出的点,每两个点与参考点组成一个三角形
1#include <iostream>2#include <math.h>3#include <vector>4#include <string>5#include <sstream>6#include <cctype>7#include <iomanip>8 9#include <glad/glad.h>10#include <GLFW/glfw3.h>11 12/**13 * 思路:14 * 1. 解析 svg 文件中的点15 * 2. 选择一个参考点,遍历解析出的点,每两个点与参考点组成一个三角形16 * 3. 绘制这些三角形17 */18using namespace std;19 20struct Point21{22 double x;23 double y;24};25 26vector<string> tokenize(const string &path)27{28 vector<string> tokens;29 string token;30 bool readingNumber = false;31 32 for (char c : path)33 {34 if (isdigit(c) || c == '.' || c == '-' || c == '+')35 {36 readingNumber = true;37 token += c;38 }39 else40 {41 if (readingNumber)42 {43 tokens.push_back(token);44 token.clear();45 readingNumber = false;46 }47 if (isalpha(c) || c == ',')48 {49 if (c != ',')50 { // 忽略逗号51 tokens.push_back(string(1, c));52 }53 }54 }55 }56 57 if (!token.empty())58 {59 tokens.push_back(token);60 }61 62 return tokens;63}64 65vector<Point> parseSVGPath(const string &pathData)66{67 vector<Point> points;68 vector<string> tokens = tokenize(pathData);69 70 double currentX = 0.0;71 double currentY = 0.0;72 size_t i = 0;73 74 while (i < tokens.size())75 {76 string cmd = tokens[i++];77 78 if (cmd == "M" || cmd == "m")79 { // MoveTo80 double x = stod(tokens[i++]);81 double y = stod(tokens[i++]);82 83 if (cmd == "m")84 {85 currentX += x;86 currentY += y;87 }88 else89 {90 currentX = x;91 currentY = y;92 }93 points.push_back({currentX, currentY});94 }95 else if (cmd == "L" || cmd == "l")96 { // LineTo97 double x = stod(tokens[i++]);98 double y = stod(tokens[i++]);99 100 if (cmd == "l")101 {102 currentX += x;103 currentY += y;104 }105 else106 {107 currentX = x;108 currentY = y;109 }110 points.push_back({currentX, currentY});111 }112 else if (cmd == "Q" || cmd == "q")113 { // Quadratic Bezier114 // 跳过控制点(前两个参数)115 i += 2;116 double x = stod(tokens[i++]);117 double y = stod(tokens[i++]);118 119 if (cmd == "q")120 {121 currentX += x;122 currentY += y;123 }124 else125 {126 currentX = x;127 currentY = y;128 }129 points.push_back({currentX, currentY});130 }131 else if (cmd == "Z" || cmd == "z")132 { // ClosePath133 // 不需要处理坐标134 }135 else136 {137 cerr << "Unsupported command: " << cmd << endl;138 break;139 }140 }141 142 return points;143}144 145const char *vertexShaderSource = "#version 330 core\n"146 "layout (location = 0) in vec3 aPos;\n"147 "void main()\n"148 "{\n"149 " gl_Position = vec4(aPos.x, -aPos.y, aPos.z, 1.0);\n"150 "}\0";151const char *fragmentShaderSource = "#version 330 core\n"152 "out vec4 FragColor;\n"153 "void main()\n"154 "{\n"155 " FragColor = vec4(0.0f, 0.0f, 1.0f, 0.4f);\n"156 "}\n\0";157 158// 编译着色器159GLuint compileShader(GLenum type, const char *source)160{161 GLuint shader = glCreateShader(type);162 glShaderSource(shader, 1, &source, nullptr);163 glCompileShader(shader);164 165 GLint success;166 glGetShaderiv(shader, GL_COMPILE_STATUS, &success);167 if (!success)168 {169 char infoLog[512];170 glGetShaderInfoLog(shader, 512, nullptr, infoLog);171 std::cerr << "ERROR: SHADER COMPILATION\n"172 << infoLog << std::endl;173 }174 return shader;175}176 177// 创建着色器程序178GLuint createShaderProgram()179{180 GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);181 GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);182 183 GLuint shaderProgram = glCreateProgram();184 glAttachShader(shaderProgram, vertexShader);185 glAttachShader(shaderProgram, fragmentShader);186 glLinkProgram(shaderProgram);187 188 glDeleteShader(vertexShader);189 glDeleteShader(fragmentShader);190 return shaderProgram;191}192 193int main()194{195 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";196 197 vector<Point> tempPoints = parseSVGPath(pathData);198 199 cout << fixed << setprecision(2);200 cout << "Extracted Points (excluding Q/q control points):\n";201 for (const auto &p : tempPoints)202 {203 cout << "(" << p.x << ", " << p.y << ")\n";204 }205 206 // 初始化 GLFW207 if (!glfwInit())208 {209 std::cerr << "无法初始化 GLFW" << std::endl;210 return -1;211 }212 213 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);214 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);215 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);216 217 GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGL Render e.svg triangles", nullptr, nullptr);218 if (!window)219 {220 std::cerr << "无法创建窗口" << std::endl;221 glfwTerminate();222 return -1;223 }224 225 glfwMakeContextCurrent(window);226 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))227 {228 std::cerr << "无法初始化 GLAD" << std::endl;229 glfwTerminate();230 return -1;231 }232 233 GLuint shaderProgram = createShaderProgram();234 235 std::vector<float> tempVertices;236 float offsetX = -0.5f;237 float offsetY = -0.5f;238 // 添加三角形239 for (size_t i = 0; i < tempPoints.size() - 1; i++)240 {241 242 if (i == tempPoints.size() - 12)243 {244 continue;245 }246 247 tempVertices.push_back(0.0 + offsetX);248 tempVertices.push_back(0.5 + offsetY);249 tempVertices.push_back(0.0);250 tempVertices.push_back(tempPoints[i].x / 1000 + offsetX);251 tempVertices.push_back(tempPoints[i].y / 1000 + offsetY);252 tempVertices.push_back(0.0);253 tempVertices.push_back(tempPoints[i + 1].x / 1000 + offsetX);254 tempVertices.push_back(tempPoints[i + 1].y / 1000 + offsetY);255 tempVertices.push_back(0.0);256 }257 258 float *vertices = new float[tempVertices.size()];259 for (size_t i = 0; i < tempVertices.size(); ++i)260 {261 std::cout << "tempVertices[" << i << "] = " << tempVertices[i] << std::endl;262 vertices[i] = tempVertices[i];263 }264 265 // 打印 数组长度266 std::cout << "vertices size: " << tempVertices.size() << std::endl;267 268 unsigned int VBO, VAO;269 glGenVertexArrays(1, &VAO);270 glGenBuffers(1, &VBO);271 glBindVertexArray(VAO);272 273 glBindBuffer(GL_ARRAY_BUFFER, VBO);274 glBufferData(GL_ARRAY_BUFFER, tempVertices.size() * sizeof(float), vertices, GL_STATIC_DRAW);275 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);276 glEnableVertexAttribArray(0);277 glBindBuffer(GL_ARRAY_BUFFER, 0);278 glBindVertexArray(0);279 280 while (!glfwWindowShouldClose(window))281 {282 glClearColor(0.974f, 0.974f, 0.978f, 1.000f);283 glClear(GL_COLOR_BUFFER_BIT);284 285 glEnable(GL_BLEND);286 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);287 288 glUseProgram(shaderProgram);289 glBindVertexArray(VAO);290 glDrawArrays(GL_TRIANGLES, 0, tempVertices.size() / 3); // 6 indices starting at 0 -> 1 triangle291 glfwSwapBuffers(window);292 glfwPollEvents();293 }294 295 glDeleteVertexArrays(1, &VAO);296 glDeleteBuffers(1, &VBO);297 glDeleteProgram(shaderProgram);298 299 glfwTerminate();300 return 0;301}使用边缘填充算法(类似),对三角形覆盖到的区域进行填充。与 Easy Scalable Text Rendering on the GPU 博客中提到的卷绕数实现有所不同,但原理是类似的,一个像素点被扫描过的次数是奇数还是偶数,奇数则上色,这种方式是不是只能用于单色的文字?
示例代码:填充轮廓区域
1#include <iostream>2#include <math.h>3#include <vector>4#include <string>5#include <sstream>6#include <cctype>7#include <iomanip>8 9#include <glad/glad.h>10#include <GLFW/glfw3.h>11#include <glm/glm.hpp>12 13/**14 * 1. 解析 svg 文件中的点15 * 2. 选择一个参考点,遍历解析的点,每两个与参考点组成一个三角形16 * 3. 绘制对应的三角形17 * 4. 边缘填充算法,奇数重叠的区域绘制,偶数重叠的区域不绘制18 */19using namespace std;20 21struct Point22{23 double x;24 double y;25};26 27vector<string> tokenize(const string &path)28{29 vector<string> tokens;30 string token;31 bool readingNumber = false;32 33 for (char c : path)34 {35 if (isdigit(c) || c == '.' || c == '-' || c == '+')36 {37 readingNumber = true;38 token += c;39 }40 else41 {42 if (readingNumber)43 {44 tokens.push_back(token);45 token.clear();46 readingNumber = false;47 }48 if (isalpha(c) || c == ',')49 {50 if (c != ',')51 { // 忽略逗号52 tokens.push_back(string(1, c));53 }54 }55 }56 }57 58 if (!token.empty())59 {60 tokens.push_back(token);61 }62 63 return tokens;64}65 66vector<Point> parseSVGPath(const string &pathData)67{68 vector<Point> points;69 vector<string> tokens = tokenize(pathData);70 71 double currentX = 0.0;72 double currentY = 0.0;73 size_t i = 0;74 75 while (i < tokens.size())76 {77 string cmd = tokens[i++];78 79 if (cmd == "M" || cmd == "m")80 { // MoveTo81 double x = stod(tokens[i++]);82 double y = stod(tokens[i++]);83 84 if (cmd == "m")85 {86 currentX += x;87 currentY += y;88 }89 else90 {91 currentX = x;92 currentY = y;93 }94 points.push_back({currentX, currentY});95 }96 else if (cmd == "L" || cmd == "l")97 { // LineTo98 double x = stod(tokens[i++]);99 double y = stod(tokens[i++]);100 101 if (cmd == "l")102 {103 currentX += x;104 currentY += y;105 }106 else107 {108 currentX = x;109 currentY = y;110 }111 points.push_back({currentX, currentY});112 }113 else if (cmd == "Q" || cmd == "q")114 { // Quadratic Bezier115 // 跳过控制点(前两个参数)116 i += 2;117 double x = stod(tokens[i++]);118 double y = stod(tokens[i++]);119 120 if (cmd == "q")121 {122 currentX += x;123 currentY += y;124 }125 else126 {127 currentX = x;128 currentY = y;129 }130 points.push_back({currentX, currentY});131 }132 else if (cmd == "Z" || cmd == "z")133 { // ClosePath134 // 不需要处理坐标135 }136 else137 {138 cerr << "Unsupported command: " << cmd << endl;139 break;140 }141 }142 143 return points;144}145 146const char *accumVertexShaderSource = R"(147#version 330 core148layout (location = 0) in vec2 aPos;149void main() {150 gl_Position = vec4(aPos, 0.0, 1.0);151})";152 153const char *accumFragmentShaderSource = R"(154#version 330 core155out vec4 FragColor;156void main() {157 FragColor = vec4(1.0/255.0, 0.0, 0.0, 1.0);158})";159 160const char *displayVertexShaderSource = R"(161#version 330 core162layout (location = 0) in vec2 aPos;163out vec2 TexCoord;164void main() {165 gl_Position = vec4(aPos, 0.0, 1.0);166 TexCoord = aPos * 0.5 + 0.5;167})";168 169const char *displayFragmentShaderSource = R"(170#version 330 core171in vec2 TexCoord;172out vec4 FragColor;173uniform sampler2D accumTexture;174void main() {175 float value = texture(accumTexture, TexCoord).r;176 int count = int(round(value * 255.0));177 FragColor = (count % 2 == 1) ? vec4(0.0f, 0.0f, 1.0f, 1.0f) : vec4(0.974f, 0.974f, 0.978f, 1.000f);178})";179 180GLuint compileShader(GLenum type, const char *source)181{182 GLuint shader = glCreateShader(type);183 glShaderSource(shader, 1, &source, nullptr);184 glCompileShader(shader);185 186 GLint success;187 glGetShaderiv(shader, GL_COMPILE_STATUS, &success);188 if (!success)189 {190 char infoLog[512];191 glGetShaderInfoLog(shader, 512, nullptr, infoLog);192 std::cerr << "Shader Error:\n"193 << infoLog << std::endl;194 }195 return shader;196}197 198GLuint createProgram(const char *vertSrc, const char *fragSrc)199{200 GLuint vert = compileShader(GL_VERTEX_SHADER, vertSrc);201 GLuint frag = compileShader(GL_FRAGMENT_SHADER, fragSrc);202 GLuint program = glCreateProgram();203 glAttachShader(program, vert);204 glAttachShader(program, frag);205 glLinkProgram(program);206 207 GLint success;208 glGetProgramiv(program, GL_LINK_STATUS, &success);209 if (!success)210 {211 char infoLog[512];212 glGetProgramInfoLog(program, 512, nullptr, infoLog);213 std::cerr << "Program Error:\n"214 << infoLog << std::endl;215 }216 glDeleteShader(vert);217 glDeleteShader(frag);218 return program;219}220 221int main()222{223 // 初始化GLFW224 if (!glfwInit())225 {226 std::cerr << "Failed to initialize GLFW" << std::endl;227 return -1;228 }229 230 // 配置GLFW231 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);232 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);233 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);234 235 // 创建窗口236 GLFWwindow *window = glfwCreateWindow(800, 600, "e.svg fill", nullptr, nullptr);237 if (!window)238 {239 std::cerr << "Failed to create GLFW window" << std::endl;240 glfwTerminate();241 return -1;242 }243 244 // 设置上下文245 glfwMakeContextCurrent(window);246 247 // 初始化GLAD248 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))249 {250 std::cerr << "Failed to initialize GLAD" << std::endl;251 return -1;252 }253 254 // 设置视口255 glViewport(0, 0, 800, 600);256 257 // 创建离屏FBO258 GLuint fbo;259 glGenFramebuffers(1, &fbo);260 glBindFramebuffer(GL_FRAMEBUFFER, fbo);261 262 // 创建累积纹理263 GLuint accumTexture;264 glGenTextures(1, &accumTexture);265 glBindTexture(GL_TEXTURE_2D, accumTexture);266 glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 800, 600, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);267 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);268 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);269 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, accumTexture, 0);270 271 // 检查FBO完整性272 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)273 {274 std::cerr << "Framebuffer is not complete!" << std::endl;275 }276 277 // 创建着色器程序278 GLuint accumProgram = createProgram(accumVertexShaderSource, accumFragmentShaderSource);279 GLuint displayProgram = createProgram(displayVertexShaderSource, displayFragmentShaderSource);280 281 std::vector<glm::vec2> vertices;282 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";283 vector<Point> tempPoints = parseSVGPath(pathData);284 285 float offsetX = -0.5f;286 float offsetY = -0.5f;287 // 添加三角形288 for (size_t i = 0; i < tempPoints.size() - 1; i++)289 {290 if (i == tempPoints.size() - 12)291 {292 continue;293 }294 vertices.push_back({-1.0 + offsetX, -(0.5 + offsetY)});295 vertices.push_back({tempPoints[i].x / 1000 + offsetX, -(tempPoints[i].y / 1000 + offsetY)});296 vertices.push_back({tempPoints[i + 1].x / 1000 + offsetX, -(tempPoints[i + 1].y / 1000 + offsetY)});297 }298 299 // 设置VAO/VBO300 GLuint vao, vbo;301 glGenVertexArrays(1, &vao);302 glGenBuffers(1, &vbo);303 glBindVertexArray(vao);304 glBindBuffer(GL_ARRAY_BUFFER, vbo);305 glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(glm::vec2), vertices.data(), GL_STATIC_DRAW);306 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *)0);307 glEnableVertexAttribArray(0);308 309 // 全屏四边形数据310 float quadVertices[] = {311 -1.0f, 1.0f, // 左上312 -1.0f, -1.0f, // 左下313 1.0f, -1.0f, // 右下314 -1.0f, 1.0f, // 左上315 1.0f, -1.0f, // 右下316 1.0f, 1.0f // 右上317 };318 319 // 设置全屏四边形VAO/VBO320 GLuint quadVAO, quadVBO;321 glGenVertexArrays(1, &quadVAO);322 glGenBuffers(1, &quadVBO);323 glBindVertexArray(quadVAO);324 glBindBuffer(GL_ARRAY_BUFFER, quadVBO);325 glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);326 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *)0);327 glEnableVertexAttribArray(0);328 329 // 主循环330 while (!glfwWindowShouldClose(window))331 {332 // 处理输入333 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)334 glfwSetWindowShouldClose(window, true);335 336 // 第一步:累积绕数337 glBindFramebuffer(GL_FRAMEBUFFER, fbo);338 glClearColor(0.974f, 0.974f, 0.978f, 1.000f);339 glClear(GL_COLOR_BUFFER_BIT);340 341 glUseProgram(accumProgram);342 glEnable(GL_BLEND);343 glBlendFunc(GL_ONE, GL_ONE);344 345 glBindVertexArray(vao);346 glDrawArrays(GL_TRIANGLES, 0, vertices.size());347 348 glDisable(GL_BLEND);349 350 // 第二步:显示结果351 glBindFramebuffer(GL_FRAMEBUFFER, 0);352 glClearColor(0.0f, 0.0f, 1.0f, 1.0f);353 glClear(GL_COLOR_BUFFER_BIT);354 355 glUseProgram(displayProgram);356 glBindTexture(GL_TEXTURE_2D, accumTexture);357 glUniform1i(glGetUniformLocation(displayProgram, "accumTexture"), 0);358 359 glBindVertexArray(quadVAO);360 glDrawArrays(GL_TRIANGLES, 0, 6);361 362 // 交换缓冲区和轮询事件363 glfwSwapBuffers(window);364 glfwPollEvents();365 }366 367 // 清理资源368 glDeleteVertexArrays(1, &vao);369 glDeleteBuffers(1, &vbo);370 glDeleteVertexArrays(1, &quadVAO);371 glDeleteBuffers(1, &quadVBO);372 glDeleteTextures(1, &accumTexture);373 glDeleteFramebuffers(1, &fbo);374 glDeleteProgram(accumProgram);375 glDeleteProgram(displayProgram);376 377 glfwTerminate();378 return 0;379}在解析点的过程中,对二次贝塞尔曲线进行插值,使其轮廓更加细密。
示例代码:填充轮廓区域
1#include <iostream>2#include <math.h>3#include <vector>4#include <string>5#include <sstream>6#include <cctype>7#include <iomanip>8 9#include <glad/glad.h>10#include <GLFW/glfw3.h>11#include <glm/glm.hpp>12 13/**14 * 1. 解析 svg 文件中的点,对二次贝塞尔曲线进行插值15 * 2. 选择一个参考点,遍历解析的点,每两个与参考点组成一个三角形16 * 3. 绘制对应的三角形17 * 4. 边缘填充算法,奇数重叠的区域绘制,偶数重叠的区域不绘制18 */19using namespace std;20 21// 2D 点结构22struct Point23{24 float x, y;25};26 27// 一条子路径:按照顺序排列的点28using PathSegment = std::vector<Point>;29 30std::string normalizePathString(const std::string &path)31{32 std::string result;33 for (size_t i = 0; i < path.size(); i++)34 {35 char c = path[i];36 if (std::isalpha(c))37 {38 result.push_back(' ');39 result.push_back(c);40 result.push_back(' ');41 }42 else43 {44 result.push_back(c);45 }46 }47 return result;48}49 50std::vector<PathSegment> parseSVGPath(const std::string &pathDataIn)51{52 std::string pathData = normalizePathString(pathDataIn);53 std::vector<PathSegment> segments;54 PathSegment currentSegment;55 56 Point current = {0.0f, 0.0f};57 Point startPoint = {0.0f, 0.0f};58 59 std::istringstream stream(pathData);60 std::string token;61 while (stream >> token)62 {63 char cmd = token[0];64 switch (cmd)65 {66 case 'M':67 case 'm':68 {69 float x, y;70 stream >> x >> y;71 Point p;72 if (cmd == 'm')73 {74 p.x = current.x + x;75 p.y = current.y + y;76 }77 else78 {79 p.x = x;80 p.y = y;81 }82 if (!currentSegment.empty())83 {84 segments.push_back(currentSegment);85 currentSegment.clear();86 }87 current = p;88 startPoint = p;89 currentSegment.push_back(p);90 break;91 }92 case 'L':93 case 'l':94 {95 float x, y;96 stream >> x >> y;97 Point p;98 if (cmd == 'l')99 {100 p.x = current.x + x;101 p.y = current.y + y;102 }103 else104 {105 p.x = x;106 p.y = y;107 }108 current = p;109 currentSegment.push_back(p);110 break;111 }112 case 'Q':113 case 'q':114 {115 float cx, cy, ex, ey;116 stream >> cx >> cy >> ex >> ey;117 Point control, end;118 if (cmd == 'q')119 {120 control.x = current.x + cx;121 control.y = current.y + cy;122 end.x = current.x + ex;123 end.y = current.y + ey;124 }125 else126 {127 control.x = cx;128 control.y = cy;129 end.x = ex;130 end.y = ey;131 }132 const int samples = 20;133 for (int i = 1; i <= samples; i++)134 {135 float t = float(i) / samples;136 float inv = 1.0f - t;137 float x = inv * inv * current.x + 2 * inv * t * control.x + t * t * end.x;138 float y = inv * inv * current.y + 2 * inv * t * control.y + t * t * end.y;139 currentSegment.push_back({x, y});140 }141 current = end;142 break;143 }144 case 'Z':145 case 'z':146 {147 // 闭合路径:将起点加入末尾148 currentSegment.push_back(startPoint);149 segments.push_back(currentSegment);150 currentSegment.clear();151 current = startPoint;152 break;153 }154 default:155 std::cerr << "不支持的命令: " << cmd << std::endl;156 break;157 }158 }159 if (!currentSegment.empty())160 segments.push_back(currentSegment);161 return segments;162}163 164const char *accumVertexShaderSource = R"(165#version 330 core166layout (location = 0) in vec2 aPos;167void main() {168 gl_Position = vec4(aPos, 0.0, 1.0);169})";170 171const char *accumFragmentShaderSource = R"(172#version 330 core173out vec4 FragColor;174void main() {175 FragColor = vec4(1.0/255.0, 0.0, 0.0, 1.0);176})";177 178const char *displayVertexShaderSource = R"(179#version 330 core180layout (location = 0) in vec2 aPos;181out vec2 TexCoord;182void main() {183 gl_Position = vec4(aPos, 0.0, 1.0);184 TexCoord = aPos * 0.5 + 0.5;185})";186 187const char *displayFragmentShaderSource = R"(188#version 330 core189in vec2 TexCoord;190out vec4 FragColor;191uniform sampler2D accumTexture;192void main() {193 float value = texture(accumTexture, TexCoord).r;194 int count = int(round(value * 255.0));195 FragColor = (count % 2 == 1) ? vec4(0.0f, 0.0f, 1.0f, 1.0f) : vec4(0.974f, 0.974f, 0.978f, 1.000f);196})";197 198GLuint compileShader(GLenum type, const char *source)199{200 GLuint shader = glCreateShader(type);201 glShaderSource(shader, 1, &source, nullptr);202 glCompileShader(shader);203 204 GLint success;205 glGetShaderiv(shader, GL_COMPILE_STATUS, &success);206 if (!success)207 {208 char infoLog[512];209 glGetShaderInfoLog(shader, 512, nullptr, infoLog);210 std::cerr << "Shader Error:\n"211 << infoLog << std::endl;212 }213 return shader;214}215 216GLuint createProgram(const char *vertSrc, const char *fragSrc)217{218 GLuint vert = compileShader(GL_VERTEX_SHADER, vertSrc);219 GLuint frag = compileShader(GL_FRAGMENT_SHADER, fragSrc);220 GLuint program = glCreateProgram();221 glAttachShader(program, vert);222 glAttachShader(program, frag);223 glLinkProgram(program);224 225 GLint success;226 glGetProgramiv(program, GL_LINK_STATUS, &success);227 if (!success)228 {229 char infoLog[512];230 glGetProgramInfoLog(program, 512, nullptr, infoLog);231 std::cerr << "Program Error:\n"232 << infoLog << std::endl;233 }234 glDeleteShader(vert);235 glDeleteShader(frag);236 return program;237}238 239int main()240{241 // 初始化GLFW242 if (!glfwInit())243 {244 std::cerr << "Failed to initialize GLFW" << std::endl;245 return -1;246 }247 248 // 配置GLFW249 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);250 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);251 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);252 253 // 创建窗口254 GLFWwindow *window = glfwCreateWindow(800, 600, "e.svg conic fill", nullptr, nullptr);255 if (!window)256 {257 std::cerr << "Failed to create GLFW window" << std::endl;258 glfwTerminate();259 return -1;260 }261 262 // 设置上下文263 glfwMakeContextCurrent(window);264 265 // 初始化GLAD266 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))267 {268 std::cerr << "Failed to initialize GLAD" << std::endl;269 return -1;270 }271 272 // 设置视口273 glViewport(0, 0, 800, 600);274 275 // 创建离屏FBO276 GLuint fbo;277 glGenFramebuffers(1, &fbo);278 glBindFramebuffer(GL_FRAMEBUFFER, fbo);279 280 // 创建累积纹理281 GLuint accumTexture;282 glGenTextures(1, &accumTexture);283 glBindTexture(GL_TEXTURE_2D, accumTexture);284 glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 800, 600, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);285 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);286 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);287 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, accumTexture, 0);288 289 // 检查FBO完整性290 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)291 {292 std::cerr << "Framebuffer is not complete!" << std::endl;293 }294 295 // 创建着色器程序296 GLuint accumProgram = createProgram(accumVertexShaderSource, accumFragmentShaderSource);297 GLuint displayProgram = createProgram(displayVertexShaderSource, displayFragmentShaderSource);298 299 std::vector<glm::vec2> vertices;300 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";301 std::vector<PathSegment> segments = parseSVGPath(pathData);302 if (segments.empty())303 {304 std::cerr << "未解析出路径数据!" << std::endl;305 return -1;306 }307 308 float offsetX = -0.5f;309 float offsetY = -0.5f;310 // 添加三角形311 for (const auto &segment : segments)312 {313 for (size_t i = 0; i < segment.size() - 1; i++)314 {315 vertices.push_back({-1.0 + offsetX, -(0.5 + offsetY)});316 vertices.push_back({segment[i].x / 1000 + offsetX, -(segment[i].y / 1000 + offsetY)});317 vertices.push_back({segment[i + 1].x / 1000 + offsetX, -(segment[i + 1].y / 1000 + offsetY)});318 }319 }320 321 // 设置VAO/VBO322 GLuint vao, vbo;323 glGenVertexArrays(1, &vao);324 glGenBuffers(1, &vbo);325 glBindVertexArray(vao);326 glBindBuffer(GL_ARRAY_BUFFER, vbo);327 glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(glm::vec2), vertices.data(), GL_STATIC_DRAW);328 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *)0);329 glEnableVertexAttribArray(0);330 331 // 全屏四边形数据332 float quadVertices[] = {333 -1.0f, 1.0f, // 左上334 -1.0f, -1.0f, // 左下335 1.0f, -1.0f, // 右下336 -1.0f, 1.0f, // 左上337 1.0f, -1.0f, // 右下338 1.0f, 1.0f // 右上339 };340 341 // 设置全屏四边形VAO/VBO342 GLuint quadVAO, quadVBO;343 glGenVertexArrays(1, &quadVAO);344 glGenBuffers(1, &quadVBO);345 glBindVertexArray(quadVAO);346 glBindBuffer(GL_ARRAY_BUFFER, quadVBO);347 glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);348 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *)0);349 glEnableVertexAttribArray(0);350 351 // 主循环352 while (!glfwWindowShouldClose(window))353 {354 // 处理输入355 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)356 glfwSetWindowShouldClose(window, true);357 358 // 第一步:累积绕数359 glBindFramebuffer(GL_FRAMEBUFFER, fbo);360 glClearColor(0.974f, 0.974f, 0.978f, 1.000f);361 glClear(GL_COLOR_BUFFER_BIT);362 363 glUseProgram(accumProgram);364 glEnable(GL_BLEND);365 glBlendFunc(GL_ONE, GL_ONE);366 367 glBindVertexArray(vao);368 glDrawArrays(GL_TRIANGLES, 0, vertices.size());369 370 glDisable(GL_BLEND);371 372 // 第二步:显示结果373 glBindFramebuffer(GL_FRAMEBUFFER, 0);374 glClearColor(0.0f, 0.0f, 1.0f, 1.0f);375 glClear(GL_COLOR_BUFFER_BIT);376 377 glUseProgram(displayProgram);378 glBindTexture(GL_TEXTURE_2D, accumTexture);379 glUniform1i(glGetUniformLocation(displayProgram, "accumTexture"), 0);380 381 glBindVertexArray(quadVAO);382 glDrawArrays(GL_TRIANGLES, 0, 6);383 384 // 交换缓冲区和轮询事件385 glfwSwapBuffers(window);386 glfwPollEvents();387 }388 389 // 清理资源390 glDeleteVertexArrays(1, &vao);391 glDeleteBuffers(1, &vbo);392 glDeleteVertexArrays(1, &quadVAO);393 glDeleteBuffers(1, &quadVBO);394 glDeleteTextures(1, &accumTexture);395 glDeleteFramebuffers(1, &fbo);396 glDeleteProgram(accumProgram);397 glDeleteProgram(displayProgram);398 399 glfwTerminate();400 return 0;401}轮廓区域填充本质上是对多边形进行填充,其核心挑战在于如何将多边形高效三角剖分为多个三角形(供 OpenGL 光栅化处理)。除前述方法外,还存在多种三角剖分算法,例如耳切法(Ear Clipping)。
示例代码:使用耳切算法
1#include <iostream>2#include <math.h>3#include <vector>4#include <string>5#include <sstream>6#include <cctype>7#include <cstdlib>8#include <limits>9#include <utility>10#include <cmath>11#include <algorithm>12 13#include <glad/glad.h>14#include <GLFW/glfw3.h>15 16 17/**18 * 总结:19 * 1. 解析 SVG 路径数据,支持 M/m、L/l、Q/q 和 Z/z 命令。20 * 2. 使用耳切算法对闭合简单多边形进行三角剖分,返回顶点数据。21 * 3. 计算多边形面积,用于判断外部轮廓和内孔。22 * 4. 计算正交变换矩阵,将所有子路径归一化到 [-1,1] 范围。23 * 5. 使用 OpenGL 渲染外部轮廓和内孔,外部轮廓填充为黄色,内孔用背景色覆盖实现镂空效果。24 * 6. 绘制所有轮廓线条,外部轮廓为黄色,内孔为背景色,轮廓线为白色。25 */26 27// 2D 点结构28struct Point {29 float x, y;30};31 32// 一条子路径:按照顺序排列的点33using PathSegment = std::vector<Point>;34 35const float EPSILON = 0.001f;36 37// ---------------------- SVG Path 解析器 ----------------------38// 支持 M/m、L/l、Q/q 和 Z/z 命令。为了方便解析,39// 先在命令字母前后加入空格,使得可以按 token 逐个读取。40 41std::string normalizePathString(const std::string& path) {42 std::string result;43 for (size_t i = 0; i < path.size(); i++) {44 char c = path[i];45 if (std::isalpha(c)) {46 result.push_back(' ');47 result.push_back(c);48 result.push_back(' ');49 } else {50 result.push_back(c);51 }52 }53 return result;54}55 56std::vector<PathSegment> parseSVGPath(const std::string& pathDataIn) {57 std::string pathData = normalizePathString(pathDataIn);58 std::vector<PathSegment> segments;59 PathSegment currentSegment;60 61 Point current = {0.0f, 0.0f};62 Point startPoint = {0.0f, 0.0f};63 64 std::istringstream stream(pathData);65 std::string token;66 while (stream >> token) {67 char cmd = token[0];68 switch (cmd) {69 case 'M':70 case 'm': {71 float x, y;72 stream >> x >> y;73 Point p;74 if (cmd == 'm') {75 p.x = current.x + x;76 p.y = current.y + y;77 } else {78 p.x = x; p.y = y;79 }80 if (!currentSegment.empty()) {81 segments.push_back(currentSegment);82 currentSegment.clear();83 }84 current = p;85 startPoint = p;86 currentSegment.push_back(p);87 break;88 }89 case 'L':90 case 'l': {91 float x, y;92 stream >> x >> y;93 Point p;94 if (cmd == 'l') {95 p.x = current.x + x;96 p.y = current.y + y;97 } else {98 p.x = x; p.y = y;99 }100 current = p;101 currentSegment.push_back(p);102 break;103 }104 case 'Q':105 case 'q': {106 float cx, cy, ex, ey;107 stream >> cx >> cy >> ex >> ey;108 Point control, end;109 if (cmd == 'q') {110 control.x = current.x + cx;111 control.y = current.y + cy;112 end.x = current.x + ex;113 end.y = current.y + ey;114 } else {115 control.x = cx; control.y = cy;116 end.x = ex; end.y = ey;117 }118 const int samples = 20;119 for (int i = 1; i <= samples; i++) {120 float t = float(i) / samples;121 float inv = 1.0f - t;122 float x = inv * inv * current.x + 2 * inv * t * control.x + t * t * end.x;123 float y = inv * inv * current.y + 2 * inv * t * control.y + t * t * end.y;124 currentSegment.push_back({x, y});125 }126 current = end;127 break;128 }129 case 'Z':130 case 'z': {131 // 闭合路径:将起点加入末尾132 currentSegment.push_back(startPoint);133 segments.push_back(currentSegment);134 currentSegment.clear();135 current = startPoint;136 break;137 }138 default:139 std::cerr << "不支持的命令: " << cmd << std::endl;140 break;141 }142 }143 if (!currentSegment.empty())144 segments.push_back(currentSegment);145 return segments;146}147 148// ---------------------- 耳切算法三角剖分 ----------------------149// 对闭合简单多边形进行三角剖分,返回排列为一维数组,每6个 float 表示一个三角形的三个顶点(x,y)。150std::vector<float> triangulatePolygon(const PathSegment& polyInput) {151 std::vector<float> result;152 if(polyInput.size() < 3)153 return result;154 155 // 复制顶点;如果首尾重复则去除末尾重复的点156 std::vector<Point> vertices = polyInput;157 if(std::fabs(vertices.front().x - vertices.back().x) < EPSILON &&158 std::fabs(vertices.front().y - vertices.back().y) < EPSILON)159 {160 vertices.pop_back();161 }162 int n = vertices.size();163 if(n < 3) return result;164 165 // 判断顶点排列是否为逆时针166 float area = 0.0f;167 for (int i = 0; i < n; i++){168 int j = (i+1) % n;169 area += vertices[i].x * vertices[j].y - vertices[j].x * vertices[i].y;170 }171 if(area < 0)172 std::reverse(vertices.begin(), vertices.end());173 174 // 用 indices 记录当前多边形175 std::vector<int> indices(n);176 for (int i=0; i<n; i++) {177 indices[i] = i;178 }179 180 auto isConvex = [&](int i0, int i1, int i2) -> bool {181 Point a = vertices[i0], b = vertices[i1], c = vertices[i2];182 float cross = (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x);183 return cross > 0;184 };185 186 auto pointInTriangle = [&](const Point &p, const Point &a, const Point &b, const Point &c) -> bool {187 auto sign = [](const Point &p1, const Point &p2, const Point &p3) {188 return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);189 };190 float d1 = sign(p, a, b);191 float d2 = sign(p, b, c);192 float d3 = sign(p, c, a);193 bool hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);194 bool hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);195 return !(hasNeg && hasPos);196 };197 198 while(indices.size() > 2) {199 bool earFound = false;200 for (size_t i = 0; i < indices.size(); i++) {201 int prev = indices[(i + indices.size() - 1) % indices.size()];202 int curr = indices[i];203 int next = indices[(i + 1) % indices.size()];204 if (!isConvex(prev, curr, next))205 continue;206 bool hasPointInside = false;207 for (size_t j = 0; j < indices.size(); j++) {208 if (j == ((i + indices.size() - 1) % indices.size()) ||209 j == i ||210 j == ((i + 1) % indices.size()))211 continue;212 int idx = indices[j];213 if(pointInTriangle(vertices[idx], vertices[prev], vertices[curr], vertices[next])) {214 hasPointInside = true;215 break;216 }217 }218 if(hasPointInside)219 continue;220 // 找到一个耳,记录三角形221 result.push_back(vertices[prev].x);222 result.push_back(vertices[prev].y);223 result.push_back(vertices[curr].x);224 result.push_back(vertices[curr].y);225 result.push_back(vertices[next].x);226 result.push_back(vertices[next].y);227 indices.erase(indices.begin() + i);228 earFound = true;229 break;230 }231 if(!earFound)232 break;233 }234 return result;235}236 237// ---------------------- 计算多边形面积 ----------------------238// 用于判断哪个子路径作为外部轮廓、哪些作为内孔。239float polygonArea(const PathSegment &seg) {240 float area = 0.0f;241 // 若首尾重复则只计算一次242 int n = seg.size();243 if(n < 3) return 0.0f;244 if (std::fabs(seg.front().x - seg.back().x) < EPSILON &&245 std::fabs(seg.front().y - seg.back().y) < EPSILON)246 n--;247 for (int i = 0; i < n; i++){248 int j = (i + 1) % n;249 area += seg[i].x * seg[j].y - seg[j].x * seg[i].y;250 }251 return std::fabs(area) / 2.0f;252}253 254// ---------------------- 正交变换矩阵 ----------------------255// 计算所有子路径的边界,然后构造一个将图形归一化到 [-1,1] 的 4x4 变换矩阵。256// 此处不处理 Y 翻转,翻转在顶点着色器中处理。257void computeTransformMatrix(const std::vector<PathSegment>& segments, float matrix[16]) {258 float xmin = std::numeric_limits<float>::max();259 float ymin = std::numeric_limits<float>::max();260 float xmax = std::numeric_limits<float>::lowest();261 float ymax = std::numeric_limits<float>::lowest();262 for (const auto& seg : segments) {263 for (const auto& p : seg) {264 if(p.x < xmin) xmin = p.x;265 if(p.x > xmax) xmax = p.x;266 if(p.y < ymin) ymin = p.y;267 if(p.y > ymax) ymax = p.y;268 }269 }270 float scaleX = 1.0f / (xmax - xmin);271 float scaleY = 1.0f / (ymax - ymin);272 float scale = (scaleX < scaleY) ? scaleX : scaleY;273 float tx = -(xmax + xmin) / 2.0f;274 float ty = -(ymax + ymin) / 2.0f;275 for (int i = 0; i < 16; i++) matrix[i] = 0.0f;276 matrix[0] = scale;277 matrix[5] = scale;278 matrix[10] = 1.0f;279 matrix[12] = scale * tx;280 matrix[13] = scale * ty;281 matrix[15] = 1.0f;282}283 284// ---------------------- OpenGL 着色器 ----------------------285// 顶点着色器:乘以变换矩阵后再对 Y 坐标取反,实现垂直翻转。286const char* vertexShaderSource = R"(287#version 330 core288layout (location = 0) in vec2 aPos;289uniform mat4 uTransform;290void main() {291 vec4 pos = uTransform * vec4(aPos, 0.0, 1.0);292 pos.y = -pos.y; // Y 轴翻转293 gl_Position = pos;294}295)";296 297// 片元着色器:输出 uniform 指定的颜色。298const char* fragmentShaderSource = R"(299#version 330 core300uniform vec4 uColor;301out vec4 FragColor;302void main(){303 FragColor = uColor;304}305)";306 307GLuint createShaderProgram() {308 auto compileShader = [&](GLenum type, const char* source) -> GLuint {309 GLuint shader = glCreateShader(type);310 glShaderSource(shader, 1, &source, nullptr);311 glCompileShader(shader);312 int success;313 glGetShaderiv(shader, GL_COMPILE_STATUS, &success);314 if (!success) {315 char infoLog[512];316 glGetShaderInfoLog(shader, 512, nullptr, infoLog);317 std::cerr << "Shader Compile Error:\n" << infoLog << std::endl;318 }319 return shader;320 };321 GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSource);322 GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);323 GLuint prog = glCreateProgram();324 glAttachShader(prog, vs);325 glAttachShader(prog, fs);326 glLinkProgram(prog);327 int success;328 glGetProgramiv(prog, GL_LINK_STATUS, &success);329 if (!success) {330 char infoLog[512];331 glGetProgramInfoLog(prog, 512, nullptr, infoLog);332 std::cerr << "Shader Link Error:\n" << infoLog << std::endl;333 }334 glDeleteShader(vs);335 glDeleteShader(fs);336 return prog;337}338 339// ---------------------- 主程序 ----------------------340int main() {341 // SVG Path 数据,注意此路径(例如代表字母 "e")内部包含两个闭合子路径,外部轮廓和内孔342 std::string pathData =343 "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 "344 "q-130.54 0 -211.29 -84.07 q-80.75 -84.07 -80.75 -219.03 q0 -161.5 99.55 -325.22 "345 "q99.55 -163.72 255.53 -267.7 q155.98 -103.98 310.85 -103.98 q81.86 0 129.42 39.83 "346 "q47.57 39.83 47.57 110.61 q0 143.81 -159.29 256.64 q-159.29 112.83 -407.08 148.23 "347 "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 "348 "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 "349 "q89.6 -42.04 137.16 -81.86 q47.56 -39.82 76.33 -86.28 q28.77 -46.46 36.51 -77.44 "350 "q7.74 -30.98 7.74 -61.95 q0 -37.61 -21.02 -61.95 q-21.01 -24.34 -56.41 -24.34 "351 "q-84.07 0 -190.26 109.52 q-106.19 109.52 -161.5 257.75 l-37.61 97.35 Z";352 353 // 解析 SVG 路径数据354 std::vector<PathSegment> segments = parseSVGPath(pathData);355 if(segments.empty()){356 std::cerr << "未解析出路径数据!" << std::endl;357 return -1;358 }359 360 // 分离外部轮廓与内孔:361 // 这里我们认为面积最大的子路径为外部轮廓,其它视为内孔(镂空区域)。362 float maxArea = 0.0f;363 int outerIndex = 0;364 for (size_t i = 0; i < segments.size(); i++){365 float a = polygonArea(segments[i]);366 if(a > maxArea) { maxArea = a; outerIndex = i; }367 }368 std::vector<PathSegment> outerSegments, holeSegments;369 for (size_t i = 0; i < segments.size(); i++){370 if ((int)i == outerIndex)371 outerSegments.push_back(segments[i]);372 else373 holeSegments.push_back(segments[i]);374 }375 376 // 分别对外部轮廓和内孔使用耳切算法进行三角剖分377 std::vector<float> outerFillVertices;378 for (const auto& seg : outerSegments) {379 std::vector<float> tris = triangulatePolygon(seg);380 if(!tris.empty())381 outerFillVertices.insert(outerFillVertices.end(), tris.begin(), tris.end());382 }383 std::vector<float> holeFillVertices;384 for (const auto& seg : holeSegments) {385 std::vector<float> tris = triangulatePolygon(seg);386 if(!tris.empty())387 holeFillVertices.insert(holeFillVertices.end(), tris.begin(), tris.end());388 }389 390 // 构建轮廓顶点数据(用于绘制外部轮廓线)391 std::vector<float> outlineVertices;392 std::vector<std::pair<int,int>> drawRanges;393 for (const auto& seg : segments) {394 int startIndex = outlineVertices.size() / 2;395 for (const auto& p : seg) {396 outlineVertices.push_back(p.x);397 outlineVertices.push_back(p.y);398 }399 int count = (outlineVertices.size() / 2) - startIndex;400 drawRanges.push_back({startIndex, count});401 }402 403 // 初始化 GLFW 与 OpenGL 上下文404 if(!glfwInit()){405 std::cerr << "无法初始化 GLFW" << std::endl;406 return -1;407 }408 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);409 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);410 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);411 412 GLFWwindow* window = glfwCreateWindow(800, 600, "SVG Renderer - Hollow 'e'", nullptr, nullptr);413 if(!window){414 std::cerr << "无法创建窗口" << std::endl;415 glfwTerminate();416 return -1;417 }418 glfwMakeContextCurrent(window);419 420 if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){421 std::cerr << "无法初始化 GLAD" << std::endl;422 return -1;423 }424 425 GLuint shaderProgram = createShaderProgram();426 427 // 创建外部轮廓(填充用)VAO/VBO428 GLuint outerVAO, outerVBO;429 glGenVertexArrays(1, &outerVAO);430 glGenBuffers(1, &outerVBO);431 glBindVertexArray(outerVAO);432 glBindBuffer(GL_ARRAY_BUFFER, outerVBO);433 glBufferData(GL_ARRAY_BUFFER, outerFillVertices.size() * sizeof(float), outerFillVertices.data(), GL_STATIC_DRAW);434 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0);435 glEnableVertexAttribArray(0);436 437 // 创建内孔(填充用)VAO/VBO438 GLuint holeVAO = 0, holeVBO = 0;439 if(!holeFillVertices.empty()){440 glGenVertexArrays(1, &holeVAO);441 glGenBuffers(1, &holeVBO);442 glBindVertexArray(holeVAO);443 glBindBuffer(GL_ARRAY_BUFFER, holeVBO);444 glBufferData(GL_ARRAY_BUFFER, holeFillVertices.size() * sizeof(float), holeFillVertices.data(), GL_STATIC_DRAW);445 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0);446 glEnableVertexAttribArray(0);447 }448 449 // 创建轮廓线 VAO/VBO450 GLuint outlineVAO, outlineVBO;451 glGenVertexArrays(1, &outlineVAO);452 glGenBuffers(1, &outlineVBO);453 glBindVertexArray(outlineVAO);454 glBindBuffer(GL_ARRAY_BUFFER, outlineVBO);455 glBufferData(GL_ARRAY_BUFFER, outlineVertices.size() * sizeof(float), outlineVertices.data(), GL_STATIC_DRAW);456 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0);457 glEnableVertexAttribArray(0);458 459 // 计算变换矩阵,将图形归一化到 [-1,1],变换不处理 Y 翻转(在着色器中实现)。460 float transform[16];461 computeTransformMatrix(segments, transform);462 463 glClearColor(0.974f, 0.974f, 0.978f, 1.000f);464 glLineWidth(4.0f);465 466 GLint transformLoc = glGetUniformLocation(shaderProgram, "uTransform");467 GLint colorLoc = glGetUniformLocation(shaderProgram, "uColor");468 469 while(!glfwWindowShouldClose(window)){470 glClear(GL_COLOR_BUFFER_BIT);471 glUseProgram(shaderProgram);472 glUniformMatrix4fv(transformLoc, 1, GL_FALSE, transform);473 474 // 渲染外部轮廓填充(蓝色)475 if(!outerFillVertices.empty()){476 glUniform4f(colorLoc, 0.0f, 0.0f, 1.0f, 1.0f);477 glBindVertexArray(outerVAO);478 glDrawArrays(GL_TRIANGLES, 0, outerFillVertices.size()/2);479 }480 // 渲染内孔区域,用背景色覆盖实现镂空效果481 if(holeVAO != 0) {482 glUniform4f(colorLoc, 0.974f, 0.974f, 0.978f, 1.000f);483 glBindVertexArray(holeVAO);484 glDrawArrays(GL_TRIANGLES, 0, holeFillVertices.size()/2);485 }486 487 // 绘制所有轮廓488 glUniform4f(colorLoc, 1.0f, 0.0f, 1.0f, 1.0f);489 glBindVertexArray(outlineVAO);490 for(const auto& range : drawRanges)491 glDrawArrays(GL_LINE_STRIP, range.first, range.second);492 493 glfwSwapBuffers(window);494 glfwPollEvents();495 }496 497 glDeleteVertexArrays(1, &outerVAO);498 glDeleteBuffers(1, &outerVBO);499 if(holeVAO != 0){500 glDeleteVertexArrays(1, &holeVAO);501 glDeleteBuffers(1, &holeVBO);502 }503 glDeleteVertexArrays(1, &outlineVAO);504 glDeleteBuffers(1, &outlineVBO);505 glDeleteProgram(shaderProgram);506 glfwTerminate();507 return 0;508}
三角剖分介绍
综上,我们解析 e.svg 中的矢量路径点,将其三角剖分为多个三角形,由 OpenGL 驱动 GPU 进行光栅化处理;而 FreeType 字体引擎则可以直接在 CPU 上完成字形轮廓的光栅化。
认识 FreeType
FreeType 简介
以前做 Android 开发时,只知道 Skia 库里面对字体的处理使用的 FreeType,这可能是我学习的第一个纯 C 语言开发的库,先通过一个示例认识 FreeType。
💭 网上有大量利用 AI 工具加速学习开源库的教程,其在提升学习效率方面成效显著。就个人学习策略而言,面对庞大的开源代码库时,我倾向于采用问题驱动的方式:聚焦源码解决特定问题的核心思路及其背后的实现原理,而非通读所有细节——永远不要试图煮沸大海,否则你会精疲力竭。
至于高级语法、设计模式等知识,实则是在特定规则约束下,为优雅解决问题而自然衍生的编码成果,无需刻意学习(比如在 Android 面试中,常被问到 OkHttp 或 Retrofit 使用了什么设计模式,这其实有些本末倒置了)。
示例程序:使用 FreeType 库加载 TrueType 字体
1#include <iostream>2#include <ft2build.h>3#include FT_FREETYPE_H4 5#define STB_IMAGE_WRITE_IMPLEMENTATION6#include "include/stb_image_write.h"7 8/**9 * 思路:10 * 1. 使用 freetype 库加载字体11 * 2. 使用 stb_image_write 库保存位图为 PNG 格式12 * 3. 将 FT_Bitmap 转换为 RGBA 格式(带透明度)13 * 4. 将 FT_Bitmap 转换为 灰度格式(单通道)14 */15unsigned char *convert_to_rgba(const FT_Bitmap &bitmap)16{17 const int width = bitmap.width;18 const int height = bitmap.rows;19 unsigned char *rgba = new unsigned char[width * height * 4]; // RGBA 缓冲区20 21 for (int y = 0; y < height; ++y)22 {23 for (int x = 0; x < width; ++x)24 {25 // 原始灰度值(0-255)26 unsigned char gray = bitmap.buffer[y * bitmap.pitch + x];27 28 // 转换为 RGBA(白色文字,黑色透明背景)29 rgba[(y * width + x) * 4 + 0] = 255; // R30 rgba[(y * width + x) * 4 + 1] = 255; // G31 rgba[(y * width + x) * 4 + 2] = 255; // B32 rgba[(y * width + x) * 4 + 3] = gray; // A(透明度)33 }34 }35 return rgba;36}37 38int main()39{40 FT_Library library;41 FT_Face face;42 43 // 初始化 FreeType 库44 if (FT_Init_FreeType(&library))45 {46 std::cerr << "无法初始化 FreeType 库" << std::endl;47 return 1;48 }49 // 加载字体文件(请替换为你的字体路径)50 std::cout << "CMake Source Dir: " << SOURCE_DIR << std::endl;51 std::string fontPathStr = std::string(SOURCE_DIR) + "/assets/FreeSerifItalic.ttf";52 const char *fontPath = fontPathStr.c_str();53 54 if (FT_New_Face(library, fontPath, 0, &face))55 {56 std::cerr << "无法加载字体文件: " << fontPath << std::endl;57 FT_Done_FreeType(library);58 return 1;59 }60 61 // 设置字体大小(48像素)62 FT_Set_Pixel_Sizes(face, 0, 24);63 64 // 加载字符 'e' 的字形65 if (FT_Load_Char(face, 'e', FT_LOAD_RENDER))66 {67 std::cerr << "无法加载字符字形" << std::endl;68 FT_Done_Face(face);69 FT_Done_FreeType(library);70 return 1;71 }72 73 // 获取字形位图信息74 FT_Bitmap bitmap = face->glyph->bitmap;75 // 保存 btimap 为 .png 文件76 77 // 打印位图信息78 std::cout << "字符 'A' 的位图信息:\n"79 << "宽度: " << bitmap.width << " 像素\n"80 << "高度: " << bitmap.rows << " 像素\n"81 << "像素模式: " << (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY ? "8位灰度" : "其他")82 << std::endl;83 84 // 转换为 RGBA85 unsigned char *rgba_data = convert_to_rgba(bitmap);86 87 // 保存为 PNG(反轉 Y 軸以符合图像坐标系)88 stbi_flip_vertically_on_write(1);89 stbi_write_png("output.png",90 bitmap.width,91 bitmap.rows,92 4, // RGBA 通道数93 rgba_data,94 bitmap.width * 4); // 每行字节数95 96 // 直接保存灰度数据(无需转换)97 stbi_write_png("gray.png",98 bitmap.width,99 bitmap.rows,100 1, // 单通道101 bitmap.buffer,102 bitmap.pitch); // 注意使用 pitch 而非 width103 104 // 清理资源105 FT_Done_Face(face);106 FT_Done_FreeType(library);107 108 return 0;109}使用 FreeType 解析出字符 e 的轮廓,并通过 OpenGl 绘制其轮廓。
示例程序:使用 FreeType 库加载 TrueType 字体,并绘制字符 ‘e’ 轮廓
1#include <iostream>2#include <math.h>3#include <vector>4#include <string>5#include <cctype>6 7#include <glad/glad.h>8#include <GLFW/glfw3.h>9 10// FreeType 头文件11#include <ft2build.h>12#include FT_FREETYPE_H13#include FT_OUTLINE_H14 15/**16 * 思路:17 * 1. 使用 freetype 库加载字体18 * 2. 解析轮廓数据19 * 3. 使用 opengl 绘制轮廓20 */21using namespace std;22 23// 用于保存轮廓顶点坐标的结构体24struct Vertex25{26 float x, y;27};28 29// 全局容器:保存所有解构后的轮廓数据,每个 contour 为一组连续顶点30std::vector<std::vector<Vertex>> gContours;31 32// ----------------- FT_Outline_Decompose 回调函数 ----------------- //33// 起始点回调:为新的轮廓创建一个顶点数组34int move_to_callback(const FT_Vector *to, void *user)35{36 auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user);37 contours->push_back(std::vector<Vertex>());38 Vertex v;39 // 将 26.6 定点数转换为浮点数40 v.x = to->x / 64.0f;41 v.y = to->y / 64.0f;42 contours->back().push_back(v);43 return 0;44}45 46// 直线段回调:直接添加目标点到当前轮廓47int line_to_callback(const FT_Vector *to, void *user)48{49 auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user);50 // 上一个点51 Vertex last = contours->back()[contours->back().size() - 1];52 Vertex v;53 v.x = to->x / 64.0f;54 v.y = to->y / 64.0f;55 contours->back().push_back(v);56 return 0;57}58 59// 二次贝塞尔曲线回调:使用均匀分段方法近似曲线60int conic_to_callback(const FT_Vector *control, const FT_Vector *to, void *user)61{62 auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user);63 // 上一个点64 Vertex from = contours->back().back();65 const int STEPS = 5; // 分段数,可根据需求调整66 for (int i = 1; i <= STEPS; i++)67 {68 float t = i / (float)STEPS;69 float one_minus_t = 1.0f - t;70 Vertex v;71 v.x = one_minus_t * one_minus_t * from.x +72 2 * one_minus_t * t * (control->x / 64.0f) +73 t * t * (to->x / 64.0f);74 v.y = one_minus_t * one_minus_t * from.y +75 2 * one_minus_t * t * (control->y / 64.0f) +76 t * t * (to->y / 64.0f);77 contours->back().push_back(v);78 }79 return 0;80}81 82// 三次贝塞尔曲线回调,同样均匀分段83int cubic_to_callback(const FT_Vector *control1,84 const FT_Vector *control2,85 const FT_Vector *to,86 void *user)87{88 auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user);89 Vertex from = contours->back().back();90 const int STEPS = 20;91 for (int i = 1; i <= STEPS; i++)92 {93 float t = i / (float)STEPS;94 float one_minus_t = 1.0f - t;95 Vertex v;96 v.x = one_minus_t * one_minus_t * one_minus_t * from.x +97 3 * one_minus_t * one_minus_t * t * (control1->x / 64.0f) +98 3 * one_minus_t * t * t * (control2->x / 64.0f) +99 t * t * t * (to->x / 64.0f);100 v.y = one_minus_t * one_minus_t * one_minus_t * from.y +101 3 * one_minus_t * one_minus_t * t * (control1->y / 64.0f) +102 3 * one_minus_t * t * t * (control2->y / 64.0f) +103 t * t * t * (to->y / 64.0f);104 contours->back().push_back(v);105 }106 return 0;107}108 109// 组装 FT_Outline_Decompose 需要的回调接口110FT_Outline_Funcs ftCallbacks = {111 move_to_callback,112 line_to_callback,113 conic_to_callback,114 cubic_to_callback,115 0, // shift116 0 // delta117};118 119// 顶点着色器120const char *vertexShaderSource = R"(121 #version 330 core122 layout(location = 0) in vec2 aPos;123 void main() {124 gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);125 }126 )";127 128// 片段着色器129const char *fragmentShaderSource = R"(130 #version 330 core131 out vec4 FragColor;132 void main() {133 FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色点134 }135 )";136 137// 编译着色器138GLuint compileShader(GLenum type, const char *source)139{140 GLuint shader = glCreateShader(type);141 glShaderSource(shader, 1, &source, nullptr);142 glCompileShader(shader);143 144 GLint success;145 glGetShaderiv(shader, GL_COMPILE_STATUS, &success);146 if (!success)147 {148 char infoLog[512];149 glGetShaderInfoLog(shader, 512, nullptr, infoLog);150 std::cerr << "ERROR: SHADER COMPILATION\n"151 << infoLog << std::endl;152 }153 return shader;154}155 156// 创建着色器程序157GLuint createShaderProgram()158{159 GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);160 GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);161 162 GLuint shaderProgram = glCreateProgram();163 glAttachShader(shaderProgram, vertexShader);164 glAttachShader(shaderProgram, fragmentShader);165 glLinkProgram(shaderProgram);166 167 glDeleteShader(vertexShader);168 glDeleteShader(fragmentShader);169 return shaderProgram;170}171 172int main()173{174 175 FT_Library library;176 FT_Face face;177 178 // 初始化 FreeType 库179 if (FT_Init_FreeType(&library))180 {181 std::cerr << "无法初始化 FreeType 库" << std::endl;182 return 1;183 }184 185 // 加载字体文件(请替换为你的字体路径)186 std::string fontPathStr = std::string(SOURCE_DIR) + "/assets/FreeSerifItalic.ttf";187 const char *fontPath = fontPathStr.c_str();188 189 if (FT_New_Face(library, fontPath, 0, &face))190 {191 std::cerr << "无法加载字体文件: " << fontPath << std::endl;192 FT_Done_FreeType(library);193 return 1;194 }195 196 FT_Set_Pixel_Sizes(face, 0, 48);197 if (FT_Load_Char(face, 'e', FT_LOAD_NO_BITMAP))198 {199 std::cerr << "无法加载字符字形" << std::endl;200 FT_Done_Face(face);201 FT_Done_FreeType(library);202 return 1;203 }204 205 // 清空并保存轮廓数据206 gContours.clear();207 if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE)208 {209 FT_Outline_Decompose(&face->glyph->outline, &ftCallbacks, &gContours);210 }211 else212 {213 std::cerr << "ERROR: Glyph does not contain an outline" << std::endl;214 return EXIT_FAILURE;215 }216 217 // 获取字形位图信息218 FT_Bitmap bitmap = face->glyph->bitmap;219 220 // 打印位图信息221 std::cout << "字符 'e' 的位图信息:\n"222 << "宽度: " << bitmap.width << " 像素\n"223 << "高度: " << bitmap.rows << " 像素\n"224 << "像素模式: "225 << (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY ? "8位灰度" : "其他")226 << std::endl;227 228 // 清理资源229 FT_Done_Face(face);230 FT_Done_FreeType(library);231 232 // ---------------------- GLFW 与 GLAD 初始化 ---------------------- //233 234 std::vector<float> lines;235 // 通过偏移、缩放来调整这些点,使用显示在窗口内236 float offsetX = -0.5;237 float offsetY = -0.5;238 float scale = 20.0f;239 240 for (const auto &contour : gContours)241 {242 std::vector<Vertex> tempPoints = contour;243 for (size_t i = 0; i < tempPoints.size() - 1; ++i)244 {245 lines.push_back(tempPoints[i].x / scale + offsetX);246 lines.push_back(tempPoints[i].y / scale + offsetY);247 lines.push_back(tempPoints[i + 1].x / scale + offsetX);248 lines.push_back(tempPoints[i + 1].y / scale + offsetY);249 }250 }251 252 // 打印 lines 中的点253 std::cout << "Extracted Points (excluding Q/q control points):\n";254 for (size_t i = 0; i < lines.size(); i += 2)255 {256 std::cout << "(" << lines[i] << ", " << lines[i + 1] << ")\n";257 }258 259 if (!glfwInit())260 {261 std::cerr << "无法初始化 GLFW" << std::endl;262 return -1;263 }264 265 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);266 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);267 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);268 269 GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGL Render e.svg Lines", nullptr, nullptr);270 if (!window)271 {272 std::cerr << "无法创建窗口" << std::endl;273 glfwTerminate();274 return -1;275 }276 277 glfwMakeContextCurrent(window);278 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))279 {280 std::cerr << "无法初始化 GLAD" << std::endl;281 glfwTerminate();282 return -1;283 }284 285 GLuint shaderProgram = createShaderProgram();286 287 GLuint VAO, VBO;288 glGenVertexArrays(1, &VAO);289 glGenBuffers(1, &VBO);290 291 glBindVertexArray(VAO);292 glBindBuffer(GL_ARRAY_BUFFER, VBO);293 glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(float), lines.data(), GL_STATIC_DRAW);294 295 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0);296 glEnableVertexAttribArray(0);297 298 glPointSize(4.0f);299 300 while (!glfwWindowShouldClose(window))301 {302 glClearColor(0.974f, 0.974f, 0.978f, 1.000f);303 glClear(GL_COLOR_BUFFER_BIT);304 305 glUseProgram(shaderProgram);306 glBindVertexArray(VAO);307 glDrawArrays(GL_LINES, 0, lines.size() / 2);308 309 glDrawArrays(GL_POINTS, 0, lines.size() / 2);310 311 glfwSwapBuffers(window);312 glfwPollEvents();313 }314 315 glDeleteVertexArrays(1, &VAO);316 glDeleteBuffers(1, &VBO);317 glDeleteProgram(shaderProgram);318 319 glfwDestroyWindow(window);320 glfwTerminate();321 return 0;322}也可以把解析出的点输出到 Mathematica 显示。
示例程序:输出 ‘e’ 轮廓到 Mathematica(加载标志使用 FT_LOAD_NO_SCALE,输出到 Mathematica 不进行 26.6 定点数转换)
1#include <iostream>2#include <math.h>3#include <vector>4#include <string>5#include <cctype>6 7#include <glad/glad.h>8#include <GLFW/glfw3.h>9 10// FreeType 头文件11#include <ft2build.h>12#include FT_FREETYPE_H13#include FT_OUTLINE_H14 15/**16 * 思路:17 * 1. 使用 freetype 库加载字体18 * 2. 解析轮廓数据19 * 2.1 输出到 mathematica20 * 3. 使用 opengl 绘制轮廓21 */22using namespace std;23 24// 用于保存轮廓顶点坐标的结构体25struct Vertex26{27 float x, y;28};29 30std::vector<std::string> graphics;31 32// 全局容器:保存所有解构后的轮廓数据,每个 contour 为一组连续顶点33std::vector<std::vector<Vertex>> gContours;34 35// ----------------- FT_Outline_Decompose 回调函数 ----------------- //36// 起始点回调:为新的轮廓创建一个顶点数组37int move_to_callback(const FT_Vector *to, void *user)38{39 auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user);40 contours->push_back(std::vector<Vertex>());41 Vertex v;42 // 将 26.6 定点数转换为浮点数43 v.x = to->x / 64.0f;44 v.y = to->y / 64.0f;45 contours->back().push_back(v);46 return 0;47}48 49// 直线段回调:直接添加目标点到当前轮廓50int line_to_callback(const FT_Vector *to, void *user)51{52 auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user);53 // 上一个点54 Vertex last = contours->back()[contours->back().size() - 1];55 Vertex v;56 v.x = to->x / 64.0f;57 v.y = to->y / 64.0f;58 contours->back().push_back(v);59 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");60 return 0;61}62 63// 二次贝塞尔曲线回调:使用均匀分段方法近似曲线64int conic_to_callback(const FT_Vector *control, const FT_Vector *to, void *user)65{66 auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user);67 // 上一个点68 Vertex from = contours->back().back();69 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";70 graphics.push_back(str);71 const int STEPS = 1; // 分段数,可根据需求调整72 for (int i = 1; i <= STEPS; i++)73 {74 float t = i / (float)STEPS;75 float one_minus_t = 1.0f - t;76 Vertex v;77 v.x = one_minus_t * one_minus_t * from.x +78 2 * one_minus_t * t * (control->x / 64.0f) +79 t * t * (to->x / 64.0f);80 v.y = one_minus_t * one_minus_t * from.y +81 2 * one_minus_t * t * (control->y / 64.0f) +82 t * t * (to->y / 64.0f);83 contours->back().push_back(v);84 }85 return 0;86}87 88// 三次贝塞尔曲线回调,同样均匀分段89int cubic_to_callback(const FT_Vector *control1,90 const FT_Vector *control2,91 const FT_Vector *to,92 void *user)93{94 auto *contours = static_cast<std::vector<std::vector<Vertex>> *>(user);95 Vertex from = contours->back().back();96 const int STEPS = 20;97 for (int i = 1; i <= STEPS; i++)98 {99 float t = i / (float)STEPS;100 float one_minus_t = 1.0f - t;101 Vertex v;102 v.x = one_minus_t * one_minus_t * one_minus_t * from.x +103 3 * one_minus_t * one_minus_t * t * (control1->x / 64.0f) +104 3 * one_minus_t * t * t * (control2->x / 64.0f) +105 t * t * t * (to->x / 64.0f);106 v.y = one_minus_t * one_minus_t * one_minus_t * from.y +107 3 * one_minus_t * one_minus_t * t * (control1->y / 64.0f) +108 3 * one_minus_t * t * t * (control2->y / 64.0f) +109 t * t * t * (to->y / 64.0f);110 contours->back().push_back(v);111 }112 return 0;113}114 115// 组装 FT_Outline_Decompose 需要的回调接口116FT_Outline_Funcs ftCallbacks = {117 move_to_callback,118 line_to_callback,119 conic_to_callback,120 cubic_to_callback,121 0, // shift122 0 // delta123};124 125// 顶点着色器126const char *vertexShaderSource = R"(127 #version 330 core128 layout(location = 0) in vec2 aPos;129 void main() {130 gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);131 }132 )";133 134// 片段着色器135const char *fragmentShaderSource = R"(136 #version 330 core137 out vec4 FragColor;138 void main() {139 FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色点140 }141 )";142 143// 编译着色器144GLuint compileShader(GLenum type, const char *source)145{146 GLuint shader = glCreateShader(type);147 glShaderSource(shader, 1, &source, nullptr);148 glCompileShader(shader);149 150 GLint success;151 glGetShaderiv(shader, GL_COMPILE_STATUS, &success);152 if (!success)153 {154 char infoLog[512];155 glGetShaderInfoLog(shader, 512, nullptr, infoLog);156 std::cerr << "ERROR: SHADER COMPILATION\n"157 << infoLog << std::endl;158 }159 return shader;160}161 162// 创建着色器程序163GLuint createShaderProgram()164{165 GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);166 GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);167 168 GLuint shaderProgram = glCreateProgram();169 glAttachShader(shaderProgram, vertexShader);170 glAttachShader(shaderProgram, fragmentShader);171 glLinkProgram(shaderProgram);172 173 glDeleteShader(vertexShader);174 glDeleteShader(fragmentShader);175 return shaderProgram;176}177 178int main()179{180 181 FT_Library library;182 FT_Face face;183 184 // 初始化 FreeType 库185 if (FT_Init_FreeType(&library))186 {187 std::cerr << "无法初始化 FreeType 库" << std::endl;188 return 1;189 }190 191 // 加载字体文件(请替换为你的字体路径)192 std::string fontPathStr = std::string(SOURCE_DIR) + "/assets/FreeSerifItalic.ttf";193 const char *fontPath = fontPathStr.c_str();194 195 if (FT_New_Face(library, fontPath, 0, &face))196 {197 std::cerr << "无法加载字体文件: " << fontPath << std::endl;198 FT_Done_FreeType(library);199 return 1;200 }201 202 // 设置字体大小(48像素)203 FT_Set_Pixel_Sizes(face, 0, 48);204 205 // if (FT_Load_Char(face, 'e', FT_LOAD_NO_BITMAP))206 if (FT_Load_Char(face, 'e', FT_LOAD_NO_SCALE))207 {208 std::cerr << "无法加载字符字形" << std::endl;209 FT_Done_Face(face);210 FT_Done_FreeType(library);211 return 1;212 }213 214 graphics.push_back("Graphics[{\n");215 216 // 清空并保存轮廓数据217 gContours.clear();218 if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE)219 {220 FT_Outline_Decompose(&face->glyph->outline, &ftCallbacks, &gContours);221 }222 else223 {224 std::cerr << "ERROR: Glyph does not contain an outline" << std::endl;225 return EXIT_FAILURE;226 }227 228 graphics.push_back("\n}, Axes -> True]");229 230 // 获取字形位图信息231 FT_Bitmap bitmap = face->glyph->bitmap;232 233 // 打印位图信息234 std::cout << "字符 'e' 的位图信息:\n"235 << "宽度: " << bitmap.width << " 像素\n"236 << "高度: " << bitmap.rows << " 像素\n"237 << "像素模式: "238 << (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY ? "8位灰度" : "其他")239 << std::endl;240 241 // 清理资源242 FT_Done_Face(face);243 FT_Done_FreeType(library);244 245 // ---------------------- GLFW 与 GLAD 初始化 ---------------------- //246 247 // 打印 graphics248 std::cout << "Graphics:\n";249 for (const auto &g : graphics)250 {251 std::cout << g << std::endl;252 }253 254 std::vector<float> lines;255 // 通过偏移、缩放来调整这些点,使用显示在窗口内256 float offsetX = -0.5;257 float offsetY = -0.5;258 float scale = 20.0f;259 260 for (const auto &contour : gContours)261 {262 std::vector<Vertex> tempPoints = contour;263 for (size_t i = 0; i < tempPoints.size() - 1; ++i)264 {265 lines.push_back(tempPoints[i].x / scale + offsetX);266 lines.push_back(tempPoints[i].y / scale + offsetY);267 lines.push_back(tempPoints[i + 1].x / scale + offsetX);268 lines.push_back(tempPoints[i + 1].y / scale + offsetY);269 }270 }271 272 // 打印 lines 中的点273 std::cout << "Extracted Points (excluding Q/q control points):\n";274 for (size_t i = 0; i < lines.size(); i += 2)275 {276 std::cout << "(" << lines[i] << ", " << lines[i + 1] << ")\n";277 }278 279 if (!glfwInit())280 {281 std::cerr << "无法初始化 GLFW" << std::endl;282 return -1;283 }284 285 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);286 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);287 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);288 289 GLFWwindow *window = glfwCreateWindow(800, 800, "OpenGL Render e.svg Points", nullptr, nullptr);290 if (!window)291 {292 std::cerr << "无法创建窗口" << std::endl;293 glfwTerminate();294 return -1;295 }296 297 glfwMakeContextCurrent(window);298 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))299 {300 std::cerr << "无法初始化 GLAD" << std::endl;301 glfwTerminate();302 return -1;303 }304 305 GLuint shaderProgram = createShaderProgram();306 307 GLuint VAO, VBO;308 glGenVertexArrays(1, &VAO);309 glGenBuffers(1, &VBO);310 311 glBindVertexArray(VAO);312 glBindBuffer(GL_ARRAY_BUFFER, VBO);313 glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(float), lines.data(), GL_STATIC_DRAW);314 315 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0);316 glEnableVertexAttribArray(0);317 318 glPointSize(4.0f);319 320 while (!glfwWindowShouldClose(window))321 {322 glClearColor(1.0f, 1.0f, 1.0f, 1.0f);323 glClear(GL_COLOR_BUFFER_BIT);324 325 glUseProgram(shaderProgram);326 glBindVertexArray(VAO);327 glDrawArrays(GL_LINES, 0, lines.size() / 2);328 329 glDrawArrays(GL_POINTS, 0, lines.size() / 2);330 331 glfwSwapBuffers(window);332 glfwPollEvents();333 }334 335 glDeleteVertexArrays(1, &VAO);336 glDeleteBuffers(1, &VBO);337 glDeleteProgram(shaderProgram);338 339 glfwDestroyWindow(window);340 glfwTerminate();341 return 0;342}输出结果如下
1Graphics[{2 3Line[{{358.000000, 109.000000}, {370.000000, 97.000000}}], 4 5BezierCurve[{{370.000000, 97.000000}, {326, 46}, {271, 17}}], 6 7BezierCurve[{{271.000000, 17.000000}, {216, -11}, {163, -11}}], 8 9BezierCurve[{{163.000000, -11.000000}, {104, -11}, {67, 27}}], 10 11BezierCurve[{{67.000000, 27.000000}, {31, 65}, {31, 126}}], 12 13BezierCurve[{{31.000000, 126.000000}, {31, 199}, {76, 273}}], 14 15BezierCurve[{{76.000000, 273.000000}, {121, 347}, {191, 394}}], 16 17BezierCurve[{{191.000000, 394.000000}, {262, 441}, {332, 441}}], 18 19BezierCurve[{{332.000000, 441.000000}, {369, 441}, {390, 423}}], 20 21BezierCurve[{{390.000000, 423.000000}, {412, 405}, {412, 373}}], 22 23BezierCurve[{{412.000000, 373.000000}, {412, 308}, {340, 257}}], 24 25BezierCurve[{{340.000000, 257.000000}, {268, 206}, {156, 190}}], 26 27Line[{{156.000000, 190.000000}, {128.000000, 186.000000}}], 28 29BezierCurve[{{128.000000, 186.000000}, {118, 146}, {118, 124}}], 30 31BezierCurve[{{118.000000, 124.000000}, {118, 83}, {143, 58}}], 32 33BezierCurve[{{143.000000, 58.000000}, {169, 34}, {212, 34}}], 34 35BezierCurve[{{212.000000, 34.000000}, {244, 34}, {274, 49}}], 36 37BezierCurve[{{274.000000, 49.000000}, {304, 65}, {358, 109}}], 38 39BezierCurve[{{135.000000, 208.000000}, {189, 221}, {229, 240}}], 40 41BezierCurve[{{229.000000, 240.000000}, {270, 259}, {291, 277}}], 42 43BezierCurve[{{291.000000, 277.000000}, {313, 295}, {326, 316}}], 44 45BezierCurve[{{326.000000, 316.000000}, {339, 337}, {342, 351}}], 46 47BezierCurve[{{342.000000, 351.000000}, {346, 365}, {346, 379}}], 48 49BezierCurve[{{346.000000, 379.000000}, {346, 396}, {336, 407}}], 50 51BezierCurve[{{336.000000, 407.000000}, {327, 418}, {311, 418}}], 52 53BezierCurve[{{311.000000, 418.000000}, {273, 418}, {225, 368}}], 54 55BezierCurve[{{225.000000, 368.000000}, {177, 319}, {152, 252}}], 56 57Line[{{152.000000, 252.000000}, {135.000000, 208.000000}}], 58 59}, Axes -> True]对比字体编辑软件打开的字符 e,这些点的坐标都是一致的,例如,第一个点的坐标(358.000000, 109.000000)。
不使用 FreeType,让 AI 帮我们生成一份解析 TureType 字体的代码,打印字符e的坐标点跟上面也是一样的。
示例程序:解析 TrueType 字体,打印字符 ‘e’ 轮廓点
1#include <iostream>2#include <fstream>3#include <cstdint>4#include <vector>5#include <cstring>6 7// 总结:8// 1. 该代码解析了 TrueType 字体文件(TTF)的结构,读取了 Offset Table、各个表记录(Table Records)以及 cmap、head、loca 和 glyf 表。9// 2. 通过 cmap 表查找字符 'e' 的 glyph index,并解析 glyf 表获取该 glyph 的轮廓数据。10// 3. 解析了 glyf 表中的简单字形(simple glyph)轮廓数据,包括点的坐标和 on-curve/off-curve 标记。11// 4. 最终输出了字符 'e' 的轮廓点数据以及轮廓划分信息。12 13// 辅助函数:从文件中读取大端格式数据14uint16_t readU16(std::ifstream &ifs)15{16 uint8_t bytes[2];17 ifs.read(reinterpret_cast<char *>(bytes), 2);18 return (bytes[0] << 8) | bytes[1];19}20 21int16_t readS16(std::ifstream &ifs)22{23 return static_cast<int16_t>(readU16(ifs));24}25 26uint32_t readU32(std::ifstream &ifs)27{28 uint8_t bytes[4];29 ifs.read(reinterpret_cast<char *>(bytes), 4);30 return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];31}32 33uint8_t readU8(std::ifstream &ifs)34{35 char byte;36 ifs.get(byte);37 return static_cast<uint8_t>(byte);38}39 40// TableRecord:记录 TTF 文件中每个表的信息(tag、checkSum、offset、length)41struct TableRecord42{43 char tag[5]; // 4 字节字符 + 结束符44 uint32_t checkSum;45 uint32_t offset;46 uint32_t length;47};48 49int main()50{51 std::string fontPathStr = std::string(SOURCE_DIR) + "/assets/FreeSerifItalic.ttf";52 const char *filename = fontPathStr.c_str();53 54 std::ifstream file(filename, std::ios::binary);55 if (!file)56 {57 std::cerr << "无法打开 TTF 文件: " << filename << std::endl;58 return 1;59 }60 61 // 1. 解析 Offset Table(12 字节)62 uint32_t sfntVersion = readU32(file);63 uint16_t numTables = readU16(file);64 uint16_t searchRange = readU16(file);65 uint16_t entrySelector = readU16(file);66 uint16_t rangeShift = readU16(file);67 68 // 2. 读取所有 Table Records69 std::vector<TableRecord> tables;70 for (int i = 0; i < numTables; i++)71 {72 TableRecord rec;73 file.read(rec.tag, 4);74 rec.tag[4] = '\0';75 rec.checkSum = readU32(file);76 rec.offset = readU32(file);77 rec.length = readU32(file);78 tables.push_back(rec);79 }80 81 // 辅助 lambda:通过 tag 查找对应表记录82 auto findTable = [&tables](const char *tag) -> TableRecord *83 {84 for (auto &t : tables)85 {86 if (std::strcmp(t.tag, tag) == 0)87 return &t;88 }89 return nullptr;90 };91 92 TableRecord *cmapTable = findTable("cmap");93 TableRecord *headTable = findTable("head");94 TableRecord *locaTable = findTable("loca");95 TableRecord *glyfTable = findTable("glyf");96 97 if (!cmapTable || !headTable || !locaTable || !glyfTable)98 {99 std::cerr << "未找到必要的表(cmap/head/loca/glyf)。" << std::endl;100 return 1;101 }102 103 // 3. 解析 head 表:获取 indexToLocFormat(位于 head 表偏移 50 处)104 file.seekg(headTable->offset + 50, std::ios::beg);105 int16_t indexToLocFormat = readS16(file);106 // std::cout << "indexToLocFormat = " << indexToLocFormat << std::endl;107 108 // 4. 解析 cmap 表:根据字符 'e' (Unicode 101) 查找对应的 glyph index109 file.seekg(cmapTable->offset, std::ios::beg);110 uint16_t cmapVersion = readU16(file);111 uint16_t numSubtables = readU16(file);112 113 uint32_t targetSubtableOffset = 0;114 // 遍历 subtables,查找平台 ID 为 3、编码 ID 为 1 且 format 为 4 的子表115 for (int i = 0; i < numSubtables; i++)116 {117 uint16_t platformID = readU16(file);118 uint16_t encodingID = readU16(file);119 uint32_t subtableOffset = readU32(file);120 std::streampos currPos = file.tellg();121 122 // 跳转到该子表检查 format 字段123 file.seekg(cmapTable->offset + subtableOffset, std::ios::beg);124 uint16_t format = readU16(file);125 if (platformID == 3 && encodingID == 1 && format == 4)126 {127 targetSubtableOffset = subtableOffset;128 file.seekg(currPos, std::ios::beg);129 break;130 }131 file.seekg(currPos, std::ios::beg);132 }133 134 if (targetSubtableOffset == 0)135 {136 std::cerr << "未找到合适的 cmap 子表(平台 3,编码 1,format 4)。" << std::endl;137 return 1;138 }139 140 // 跳转到选定的 cmap 子表,并解析 format 4 数据141 file.seekg(cmapTable->offset + targetSubtableOffset, std::ios::beg);142 uint16_t format = readU16(file); // 应为 4143 uint16_t length = readU16(file);144 uint16_t language = readU16(file);145 uint16_t segCountX2 = readU16(file);146 uint16_t segCount = segCountX2 / 2;147 uint16_t searchRange_cmap = readU16(file);148 uint16_t entrySelector_cmap = readU16(file);149 uint16_t rangeShift_cmap = readU16(file);150 151 std::vector<uint16_t> endCodes(segCount);152 for (int i = 0; i < segCount; i++)153 {154 endCodes[i] = readU16(file);155 }156 157 uint16_t reservedPad = readU16(file);158 159 std::vector<uint16_t> startCodes(segCount);160 for (int i = 0; i < segCount; i++)161 {162 startCodes[i] = readU16(file);163 }164 165 std::vector<int16_t> idDeltas(segCount);166 for (int i = 0; i < segCount; i++)167 {168 idDeltas[i] = readS16(file);169 }170 171 std::vector<uint16_t> idRangeOffsets(segCount);172 for (int i = 0; i < segCount; i++)173 {174 idRangeOffsets[i] = readU16(file);175 }176 177 // 对于字符 'e' (Unicode 101)进行查找178 uint16_t unicode = 101;179 uint16_t glyphIndex = 0;180 for (int i = 0; i < segCount; i++)181 {182 if (unicode >= startCodes[i] && unicode <= endCodes[i])183 {184 if (idRangeOffsets[i] == 0)185 {186 glyphIndex = (unicode + idDeltas[i]) % 65536;187 }188 else189 {190 std::streampos pos = file.tellg();191 std::streampos glyphIndexAddr = cmapTable->offset + targetSubtableOffset + 14 + (i * 2) + idRangeOffsets[i] + (unicode - startCodes[i]) * 2;192 file.seekg(glyphIndexAddr, std::ios::beg);193 glyphIndex = readU16(file);194 if (glyphIndex != 0)195 glyphIndex = (glyphIndex + idDeltas[i]) % 65536;196 file.seekg(pos, std::ios::beg);197 }198 break;199 }200 }201 std::cout << "字符 'e' 对应的 Glyph Index = " << glyphIndex << std::endl;202 203 // 5. 解析 loca 表:获取 glyph 在 glyf 表中的偏移204 uint32_t glyphOffset, glyphNextOffset;205 if (indexToLocFormat == 0)206 {207 // short 版本,每个记录为 uint16_t,偏移需要乘 2208 file.seekg(locaTable->offset + glyphIndex * 2, std::ios::beg);209 uint16_t off = readU16(file);210 glyphOffset = off * 2;211 file.seekg(locaTable->offset + (glyphIndex + 1) * 2, std::ios::beg);212 uint16_t off2 = readU16(file);213 glyphNextOffset = off2 * 2;214 }215 else216 {217 // long 版本,每个记录为 uint32_t218 file.seekg(locaTable->offset + glyphIndex * 4, std::ios::beg);219 glyphOffset = readU32(file);220 file.seekg(locaTable->offset + (glyphIndex + 1) * 4, std::ios::beg);221 glyphNextOffset = readU32(file);222 }223 if (glyphOffset == glyphNextOffset)224 {225 std::cerr << "该 glyph 不包含轮廓数据。" << std::endl;226 return 1;227 }228 229 // 6. 解析 glyf 表中 simple glyph(简单字形)的轮廓数据230 file.seekg(glyfTable->offset + glyphOffset, std::ios::beg);231 int16_t numberOfContours = readS16(file);232 int16_t xMin = readS16(file);233 int16_t yMin = readS16(file);234 int16_t xMax = readS16(file);235 int16_t yMax = readS16(file);236 237 if (numberOfContours < 0)238 {239 std::cerr << "组合字形(composite glyph)不在本示例支持范围内。" << std::endl;240 return 1;241 }242 243 // 读取每个轮廓的终点索引244 std::vector<uint16_t> endPtsOfContours(numberOfContours);245 for (int i = 0; i < numberOfContours; i++)246 {247 endPtsOfContours[i] = readU16(file);248 }249 250 // 跳过 TrueType 指令251 uint16_t instructionLength = readU16(file);252 file.seekg(instructionLength, std::ios::cur);253 254 int numPoints = endPtsOfContours.back() + 1;255 256 // 读取每个点的 flags,注意存在重复标记机制257 std::vector<uint8_t> finalFlags;258 while (finalFlags.size() < (size_t)numPoints)259 {260 uint8_t flag = readU8(file);261 finalFlags.push_back(flag);262 if (flag & 0x08)263 { // 重复标记264 uint8_t repeatCount = readU8(file);265 for (int j = 0; j < repeatCount; j++)266 {267 finalFlags.push_back(flag);268 }269 }270 }271 272 // 读取 x 坐标(增量编码)273 std::vector<int16_t> xCoordinates(numPoints, 0);274 int16_t x = 0;275 for (int i = 0; i < numPoints; i++)276 {277 if (finalFlags[i] & 0x02)278 { // x-short vector:一个 unsigned byte279 uint8_t dx = readU8(file);280 if (finalFlags[i] & 0x10)281 { // 若设置,则为正值282 x += dx;283 }284 else285 {286 x -= dx;287 }288 }289 else290 {291 if (!(finalFlags[i] & 0x10))292 { // x not same293 int16_t dx = readS16(file);294 x += dx;295 }296 }297 xCoordinates[i] = x;298 }299 300 // 读取 y 坐标(增量编码)301 std::vector<int16_t> yCoordinates(numPoints, 0);302 int16_t y = 0;303 for (int i = 0; i < numPoints; i++)304 {305 if (finalFlags[i] & 0x04)306 { // y-short vector:一个 unsigned byte307 uint8_t dy = readU8(file);308 if (finalFlags[i] & 0x20)309 { // 若设置,则为正值310 y += dy;311 }312 else313 {314 y -= dy;315 }316 }317 else318 {319 if (!(finalFlags[i] & 0x20))320 {321 int16_t dy = readS16(file);322 y += dy;323 }324 }325 yCoordinates[i] = y;326 }327 328 // 输出轮廓数据329 std::cout << "轮廓数据(Glyph 'e'):" << std::endl;330 std::cout << "Number of Contours: " << numberOfContours << std::endl;331 std::cout << "Bounding Box: (" << xMin << ", " << yMin << ") to (" << xMax << ", " << yMax << ")" << std::endl;332 std::cout << "轮廓终点索引: ";333 for (const auto &endPt : endPtsOfContours)334 {335 std::cout << endPt << " ";336 }337 std::cout << std::endl;338 std::cout << "点坐标(x, y)及标志:" << std::endl;339 for (int i = 0; i < numPoints; i++)340 {341 std::cout << "Point " << i << ": (" << xCoordinates[i] << ", " << yCoordinates[i] << "), Flags: " << static_cast<int>(finalFlags[i]) << std::endl;342 }343 std::cout << "----------------------------------------" << std::endl;344 345 return 0;346}347 打印输出
1字符 'e' 对应的 Glyph Index = 722轮廓数据(Glyph 'e'):3Number of Contours: 24Bounding Box: (31, -11) to (412, 441)5轮廓终点索引: 25 38 6点坐标(x, y)及标志:7Point 0: (358, 109), Flags: 378Point 1: (370, 97), Flags: 239Point 2: (326, 46), Flags: 1410Point 3: (216, -11), Flags: 1411Point 4: (163, -11), Flags: 3512Point 5: (104, -11), Flags: 3413Point 6: (31, 65), Flags: 3814Point 7: (31, 126), Flags: 5315Point 8: (31, 199), Flags: 5216Point 9: (121, 347), Flags: 6217Point 10: (262, 441), Flags: 6218Point 11: (332, 441), Flags: 5119Point 12: (369, 441), Flags: 5020Point 13: (412, 405), Flags: 2221Point 14: (412, 373), Flags: 2122Point 15: (412, 308), Flags: 2023Point 16: (268, 206), Flags: 624Point 17: (156, 190), Flags: 1525Point 18: (128, 186), Flags: 1526Point 19: (118, 146), Flags: 627Point 20: (118, 124), Flags: 2128Point 21: (118, 83), Flags: 2029Point 22: (169, 34), Flags: 2230Point 23: (212, 34), Flags: 5131Point 24: (244, 34), Flags: 5032Point 25: (304, 65), Flags: 5433Point 26: (135, 208), Flags: 3934Point 27: (189, 221), Flags: 6235Point 28: (270, 259), Flags: 6236Point 29: (313, 295), Flags: 6237Point 30: (339, 337), Flags: 6238Point 31: (346, 365), Flags: 6239Point 32: (346, 379), Flags: 5340Point 33: (346, 396), Flags: 5241Point 34: (327, 418), Flags: 3842Point 35: (311, 418), Flags: 3543Point 36: (273, 418), Flags: 3444Point 37: (177, 319), Flags: 645Point 38: (152, 252), Flags: 7💭 .ttf 文件本质上是一种结构化二进制格式,其解析流程与图像、音频等媒体文件相似,首先读取文件头(Header),根据头信息定位各数据段在内存中的偏移量,进而加载对应的字形轮廓及元数据。
FreeType 字符轮廓的光栅化
在前面的内容中,我们已经了解了如何解析 .ttf 文件以获取对应字符的轮廓数据。接下来的问题是如何将这些轮廓数据进行光栅化,或者说怎么从数学描述转换到像素阵列,FreeType 中可以通过导出灰度图的方式来探索这个过程。
调试 FreeType 源码
要想明晰这个过程具体细节,不得不深入到源码进行查看,克隆 freetype 源码,修改 CMakeList.txt 文件,把导出静态库的配置修改为编译可执行项目。
1# add_library(freetype2# ${PUBLIC_HEADERS}3# ${PUBLIC_CONFIG_HEADERS}4# ${PRIVATE_HEADERS}5# ${BASE_SRCS}6# )7 8add_executable(freetype-test9 main.cpp 10 ${PUBLIC_HEADERS}11 ${PUBLIC_CONFIG_HEADERS}12 ${PRIVATE_HEADERS}13 ${BASE_SRCS})添加一个 main.cpp 文件
1#include <ft2build.h>2#include <iostream>3#include FT_FREETYPE_H4 5/**6 * 调试用7 * 1. 使用 freetype 库加载字体8 * 2. 打印位图内容9 */10int main()11{12 FT_Library library;13 FT_Face face;14 15 // 初始化 FreeType 库16 if (FT_Init_FreeType(&library))17 {18 std::cerr << "无法初始化 FreeType 库" << std::endl;19 return 1;20 }21 22 // 加载字体文件(请替换为你的字体路径)23 const char *fontPath ="../assets/FreeSerifItalic.ttf";24 if (FT_New_Face(library, fontPath, 0, &face))25 {26 std::cerr << "无法加载字体文件: " << fontPath << std::endl;27 FT_Done_FreeType(library);28 return 1;29 }30 31 // 设置字体大小(48像素)32 FT_Set_Pixel_Sizes(face, 0, 48);33 34 // 加载字符 'e'35 if (FT_Load_Char(face, 'e', FT_LOAD_RENDER))36 {37 std::cerr << "无法加载字符字形" << std::endl;38 FT_Done_Face(face);39 FT_Done_FreeType(library);40 return 1;41 }42 43 // 获取字形位图信息44 FT_Bitmap bitmap = face->glyph->bitmap;45 46 // 打印位图信息47 std::cout << "字符 'A' 的位图信息:\n"48 << "宽度: " << bitmap.width << " 像素\n"49 << "高度: " << bitmap.rows << " 像素\n"50 << "像素模式: "51 << (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY ? "8位灰度" : "其他")52 << std::endl;53 54 // 打印位图内容(ASCII)55 std::cout << "\n位图内容:\n";56 for (unsigned int y = 0; y < bitmap.rows; ++y)57 {58 for (unsigned int x = 0; x < bitmap.width; ++x)59 {60 unsigned char pixel = bitmap.buffer[y * bitmap.pitch + x];61 std::cout << (pixel > 128 ? '*' : ' '); // 用不同字符表示灰度值62 }63 std::cout << '\n';64 }65 66 // 清理资源67 FT_Done_Face(face);68 FT_Done_FreeType(library);69 70 return 0;71}💡 可以设置 freetype 日志级别,打印日志到控制台,方便我们进行调试。
你无法调试你从未理解过的东西。一些前置知识是必须的:计算机图形学 多边形扫描填充 (bilibili)、CG11-光栅化1-扫描填充 (bilibili)、计算机图形学|扫描线算法 ET表 AET表例题讲解 (bilibil)。
示例代码:扫描线填充算法
1#include <iostream>2#include <vector>3#include <algorithm>4#include <cmath>5#include <iostream>6#include <math.h>7#include <vector>8#include <string>9#include <sstream>10#include <cctype>11 12/**13 * 总结:14 * 1. 该代码实现了一个简单的SVG路径解析器,能够将SVG路径字符串转换为一系列2D点。15 * 2. 使用了扫描线填充算法来填充多边形区域。16 */17 18using namespace std;19 20// 2D 点结构21struct Point22{23 float x, y;24};25 26// 一条子路径:按照顺序排列的点27using PathSegment = std::vector<Point>;28 29std::string normalizePathString(const std::string &path)30{31 std::string result;32 for (size_t i = 0; i < path.size(); i++)33 {34 char c = path[i];35 if (std::isalpha(c))36 {37 result.push_back(' ');38 result.push_back(c);39 result.push_back(' ');40 }41 else42 {43 result.push_back(c);44 }45 }46 return result;47}48 49std::vector<PathSegment> parseSVGPath(const std::string &pathDataIn)50{51 std::string pathData = normalizePathString(pathDataIn);52 std::vector<PathSegment> segments;53 PathSegment currentSegment;54 55 Point current = {0.0f, 0.0f};56 Point startPoint = {0.0f, 0.0f};57 58 std::istringstream stream(pathData);59 std::string token;60 while (stream >> token)61 {62 char cmd = token[0];63 switch (cmd)64 {65 case 'M':66 case 'm':67 {68 float x, y;69 stream >> x >> y;70 Point p;71 if (cmd == 'm')72 {73 p.x = current.x + x;74 p.y = current.y + y;75 }76 else77 {78 p.x = x;79 p.y = y;80 }81 if (!currentSegment.empty())82 {83 segments.push_back(currentSegment);84 currentSegment.clear();85 }86 current = p;87 startPoint = p;88 currentSegment.push_back(p);89 break;90 }91 case 'L':92 case 'l':93 {94 float x, y;95 stream >> x >> y;96 Point p;97 if (cmd == 'l')98 {99 p.x = current.x + x;100 p.y = current.y + y;101 }102 else103 {104 p.x = x;105 p.y = y;106 }107 current = p;108 currentSegment.push_back(p);109 break;110 }111 case 'Q':112 case 'q':113 {114 float cx, cy, ex, ey;115 stream >> cx >> cy >> ex >> ey;116 Point control, end;117 if (cmd == 'q')118 {119 control.x = current.x + cx;120 control.y = current.y + cy;121 end.x = current.x + ex;122 end.y = current.y + ey;123 }124 else125 {126 control.x = cx;127 control.y = cy;128 end.x = ex;129 end.y = ey;130 }131 const int samples = 20;132 for (int i = 1; i <= samples; i++)133 {134 float t = float(i) / samples;135 float inv = 1.0f - t;136 float x = inv * inv * current.x + 2 * inv * t * control.x + t * t * end.x;137 float y = inv * inv * current.y + 2 * inv * t * control.y + t * t * end.y;138 currentSegment.push_back({x, y});139 }140 current = end;141 break;142 }143 case 'Z':144 case 'z':145 {146 // 闭合路径:将起点加入末尾147 currentSegment.push_back(startPoint);148 segments.push_back(currentSegment);149 currentSegment.clear();150 current = startPoint;151 break;152 }153 default:154 std::cerr << "不支持的命令: " << cmd << std::endl;155 break;156 }157 }158 if (!currentSegment.empty())159 segments.push_back(currentSegment);160 return segments;161}162 163 164 165 166// 打印画布,将二维字符数组输出到控制台167void printCanvas(const vector<vector<char>>& canvas) {168 for (size_t y = 0; y < canvas.size(); y++) {169 for (size_t x = 0; x < canvas[0].size(); x++) {170 cout << canvas[y][x];171 }172 cout << endl;173 }174}175 176// 扫描线填充算法177void scanlineFill(const vector<Point>& vertices, vector<vector<char>>& canvas, char fillChar) {178 int n = vertices.size();179 if(n < 3) return; // 多边形至少有 3 个顶点180 181 // 找出多边形顶点的最小和最大 y 值182 int yMin = canvas.size();183 int yMax = 0;184 for (const auto &pt : vertices) {185 if(pt.y < yMin) yMin = pt.y;186 if(pt.y > yMax) yMax = pt.y;187 }188 189 // 边数据结构,记录当前扫描需要的数据190 struct EdgeRec {191 double x; // 当前扫描线交点 x 坐标192 int yMax; // 边的最大 y 值(扫描到此值时,边退出活动边表)193 double invSlope; // 逆斜率 dx/dy194 int yMin; // 边的最小 y 值(开始参与扫描填充的 y 值)195 };196 197 // 构建边表:遍历所有多边形边,跳过水平边198 vector<EdgeRec> edgeTable;199 for (int i = 0; i < n; i++){200 Point cur = vertices[i];201 Point nxt = vertices[(i + 1) % n];202 // 如果为水平边,则跳过203 if (cur.y == nxt.y)204 continue;205 EdgeRec edge;206 if(cur.y < nxt.y) {207 edge.yMin = cur.y;208 edge.yMax = nxt.y;209 edge.x = cur.x;210 edge.invSlope = double(nxt.x - cur.x) / double(nxt.y - cur.y);211 } else {212 edge.yMin = nxt.y;213 edge.yMax = cur.y;214 edge.x = nxt.x;215 edge.invSlope = double(cur.x - nxt.x) / double(cur.y - nxt.y);216 }217 edgeTable.push_back(edge);218 }219 220 // 活动边表,存储当前扫描线与多边形相交的边221 vector<EdgeRec> activeEdges;222 223 // 从 ymin 扫描到 ymax - 1(如果想要严格按像素填充,也可以扫描到 yMax)224 for (int y = yMin; y < yMax; y++) {225 // 将从当前扫描线开始的新边加入 activeEdges226 for (const auto &e : edgeTable) {227 if(e.yMin == y)228 activeEdges.push_back(e);229 }230 // 删除那些已经结束的边(当前 y 已经达到或超过其 yMax)231 activeEdges.erase(remove_if(activeEdges.begin(), activeEdges.end(),232 [y](const EdgeRec &e) { return y >= e.yMax; }),233 activeEdges.end());234 235 // 对活动边表按交点 x 坐标排序236 sort(activeEdges.begin(), activeEdges.end(), [](const EdgeRec &a, const EdgeRec &b){237 return a.x < b.x;238 });239 240 // 两两配对填充扫描线内的像素,假设活动边表中的边数为偶数241 for (size_t i = 0; i < activeEdges.size(); i += 2) {242 if(i + 1 < activeEdges.size()) {243 int xStart = int(ceil(activeEdges[i].x));244 int xEnd = int(floor(activeEdges[i + 1].x));245 for (int x = xStart; x <= xEnd; x++){246 // 检查边界后填充247 if(y >= 0 && y < (int)canvas.size() && x >= 0 && x < (int)canvas[0].size())248 canvas[y][x] = fillChar;249 }250 }251 }252 253 // 更新活动边表中所有边的 x 坐标,进入下一扫描线254 for(auto &edge : activeEdges) {255 edge.x += edge.invSlope;256 }257 }258}259 260int main() {261 // 定义画布宽度和高度(例如 80x60 的字符画布)262 int width = 80;263 int height = 60;264 // 用'.'填充画布背景265 vector<vector<char>> canvas(height, vector<char>(width, '.'));266 267 268 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";269 std::vector<PathSegment> segments = parseSVGPath(pathData);270 271 if (segments.empty())272 {273 std::cerr << "未解析出路径数据!" << std::endl;274 return -1;275 }276 277 float offsetX = 0.0f;278 float offsetY = 0.0f;279 vector<Point> polygon;280 // 添加三角形281 for (const auto &segment : segments)282 {283 for (size_t i = 0; i < segment.size() - 1; i++)284 {285 // 添加点到 polygon286 polygon.push_back({(segment[i].x + offsetX)/20, (segment[i].y + offsetY)/20});287 }288 }289 290 // 使用扫描线填充算法填充多边形,填充字符为'#'291 scanlineFill(polygon, canvas, '*');292 293 // 打印填充后的画布294 printCanvas(canvas);295 296 return 0;297}298 结果输出:
1................................***********.....................................2............................****************....................................3..........................*******....********...................................4.........................******.......********..................................5.......................******..........*******..................................6.....................*******...........*******..................................7....................*******............********.................................8..................********.............********.................................9..................*******..............*******..................................10................********...............*******..................................11................*******................*******..................................12..............*********...............********..................................13..............********................********..................................14.............********................********...................................15............*********...............********....................................16............********................********....................................17..........**********...............********.....................................18..........*********..............*********......................................19.........**********.............*********.......................................20.........*********.............********.........................................21........**********...........*********..........................................22........*********..........*********............................................23.......**********........*********..............................................24.......**********.....**********................................................25.......*********...************.................................................26......*********************.....................................................27......*****************.........................................................28.....*************..............................................................29.....**********.................................................................30.....**********.................................................................31.....**********.................................................................32.....*********..................................................................33.....*********..................................................................34....**********..................................................................35....**********..................................................................36....**********..................................................................37.....*********..................................................................38.....*********.........................***......................................39.....**********.......................***.......................................40.....**********......................***........................................41.....***********...................****.........................................42......**********..................****..........................................43......***********...............*****...........................................44.......************...........******............................................45........****************..*********.............................................46........*************************...............................................47..........*********************.................................................48...........******************...................................................49.............*************......................................................50..................**............................................................51................................................................................52................................................................................53................................................................................54................................................................................55................................................................................56................................................................................57................................................................................58................................................................................59................................................................................60................................................................................💭 原来这些都是计算机图形学最基础的知识。
在调试两遍之后,即便借助 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)。确保在各种分辨率和缩放比例下都能获得清晰、一致的文本渲染效果。支持动态调整,无需重新加载字体文件。
1FT_Request_Size (truetype driver):2 x scale: 201327 (3.072006) // x 轴缩放比例3 y scale: 201327 (3.072006) // y 轴缩放比例4 ascender: 44.000000 // 从水平基线到字体面上最高“字符”坐标的垂直距离。5 descender: -15.000000 // 下从水平基线到字体面中最低“字符”坐标的垂直距离。6 height: 62.000000 // 此字段代表使用该字体编写文本时的默认线间距(即基线到基线距离)。请注意,它通常大于将其作为绝对值的升华和下降的总和。7 max advance: 77.000000 // 该字段为字体中所有字形提供了最大的水平光标步进。 8 x ppem: 48 // X 轴的点阵大小9 y ppem: 48 // Y 轴的点阵大小10 11 // 准确释义: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)
1// 解析字体文件读取的原始轮廓点数据2======>[read points] Point 0: x=358.000000, y=109.0000003======>[read points] Point 1: x=370.000000, y=97.0000004======>[read points] Point 2: x=326.000000, y=46.0000005======>[read points] Point 3: x=216.000000, y=-11.0000006======>[read points] Point 4: x=163.000000, y=-11.0000007======>[read points] Point 5: x=104.000000, y=-11.0000008======>[read points] Point 6: x=31.000000, y=65.0000009======>[read points] Point 7: x=31.000000, y=126.00000010======>[read points] Point 8: x=31.000000, y=199.00000011 12======>[read tags] Tag 0: 1 // 1 在轮廓线上的点13======>[read tags] Tag 1: 114======>[read tags] Tag 2: 0 // 0 二次贝塞尔曲线的控制点15======>[read tags] Tag 3: 016======>[read tags] Tag 4: 117======>[read tags] Tag 5: 018======>[read tags] Tag 6: 019======>[read tags] Tag 7: 120======>[read tags] Tag 8: 0- •TT_Process_Simple_Glyph()
负责处理 TrueType 简单字形(非复合字形)的最终渲染流程,主要完成:
- •幻影点设置:添加四个关键定位点(pp1-pp4)到字形轮廓
- •可变字体支持:应用 GX 变形表的字形坐标调整
- •轮廓点缩放:将设计单位转换为设备像素单位
- •字形提示处理:执行字节码提示优化小字号显示效果
1// 缩放后2======>[after scale] Point 0: x=1100.000000, y=335.0000003======>[after scale] Point 1: x=1137.000000, y=298.0000004======>[after scale] Point 2: x=1001.000000, y=141.0000005======>[after scale] Point 3: x=664.000000, y=-34.0000006======>[after scale] Point 4: x=501.000000, y=-34.0000007======>[after scale] Point 5: x=319.000000, y=-34.0000008======>[after scale] Point 6: x=95.000000, y=200.0000009======>[after scale] Point 7: x=95.000000, y=387.00000010======>[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)和内存对齐
- •坐标转换:将轮廓坐标转换为位图物理坐标
1======>[bitmap size] width:19,height:23- •FT_Outline_Translate
对缩放后的点进行位移操作
1// 位移后2======>[after translate] Point 0: x=1036.000000, y=399.0000003======>[after translate] Point 1: x=1073.000000, y=362.0000004======>[after translate] Point 2: x=937.000000, y=205.0000005======>[after translate] Point 3: x=600.000000, y=30.0000006======>[after translate] Point 4: x=437.000000, y=30.0000007======>[after translate] Point 5: x=255.000000, y=30.0000008======>[after translate] Point 6: x=31.000000, y=264.0000009======>[after translate] Point 7: x=31.000000, y=451.00000010======>[after tranlate] Point 8: x=31.000000, y=675.000000- •gray_convert_glyph()
该函数是 FreeType 中用于灰度抗锯齿字形光栅化的核心分带渲染算法,此函数实现分带式光栅化,通过垂直分带 + 水平二分策略高效处理大字形,避免内存溢出。核心流程分为:
- •垂直分带:将字形高度分割为多个垂直带
- •水平分带:对每个垂直带进行水平二分递归
- •单元格渲染:在子带内生成覆盖数据
- •像素填充:将单元格数据转换为位图像素
1ras.ycells = (PCell*)buffer; // 存储每行的单元格链表头指针,数组+链表的数据结构- •FT_Outline_Decompose()
库中 FT_Outline_Decompose 函数的详细解析,该函数是矢量字形轮廓分解的核心实现:
- •轮廓分解:将复杂的轮廓路径分解为基本图元(直线、二次/三次贝塞尔曲线)
- •坐标转换:应用缩放和偏移变换(通过 shift 和 delta 参数)
- •回调驱动:通过函数指针调用用户定义的绘图操作
- •轮廓闭合:确保每个轮廓路径正确闭合
1// 把 26.6 定点数转化成 float 类型,这些便是用于光栅化的坐标数据2======>[preparing to convert to bitmap] 0: x=16.187500, y=6.2343753======>[preparing to convert to bitmap] 1: x=16.765625, y=5.6562504======>[preparing to convert to bitmap] 2: x=14.640625, y=3.2031255======>[preparing to convert to bitmap] 3: x=9.375000, y=0.4687506======>[preparing to convert to bitmap] 4: x=6.828125, y=0.4687507======>[preparing to convert to bitmap] 5: x=3.984375, y=0.4687508======>[preparing to convert to bitmap] 6: x=0.484375, y=4.1250009======>[preparing to convert to bitmap] 7: x=0.484375, y=7.04687510======>[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 等工具一样,本质上是一种拓展能力的杠杆。你掌握的工具越多,能进行的尝试就越丰富。这些工具节省下来的时间,让你能够更系统地解决问题、有更多的时间创造出更出色的作品。
总之,切勿让人工智能代替你思考,因为编程的核心,永远在于思考本身。