1. 轮廓文字渲染的整体设计思路
1.1 为什么不用纹理贴图而选择轮廓方案
做过OpenGL文字渲染的人多半经历过这个纠结:到底是用纹理贴图把字形烘焙成一张图,还是用几何轮廓把字形拆成线段来画。我最早做HUD文字的时候,第一反应就是纹理方案——把字体渲染成一张位图,然后贴到四边形上。这个方案上手快,但一旦涉及旋转、缩放、描边,问题就全冒出来了。
纹理方案的核心痛点在于分辨率耦合。你烘焙一张64像素高的字形纹理,放大到200像素就会糊;旋转45度之后边缘锯齿明显;想加一圈描边还得额外生成一张描边纹理或者用后处理。更麻烦的是,如果文字需要动态改变粗细或者做描边动画,纹理方案基本没法优雅地实现。
轮廓文字方案走的是另一条路:把每个字符的笔画中心线提取出来,用线段(或者细长的四边形)去绘制。这样做的好处是分辨率无关——无论放大多少倍,线条始终是矢量清晰的;旋转的时候只需要对顶点做矩阵变换,不会产生任何采样失真;描边、加粗、虚线这些效果都可以通过调整线宽和绘制方式来实现。
当然,轮廓方案也有代价。它不适合表现复杂的衬线字体或者笔画粗细变化剧烈的字形,因为中心线提取本身就很难做到完美。但对于常见的无衬线字体、工程标注、仪表盘文字这类场景,轮廓方案的效果和灵活性都远超纹理方案。
1.2 可旋转这个需求背后的技术含义
标题里特别强调了“可旋转”,这不是随便加的。在OpenGL里,旋转本身只是一个矩阵乘法的事,但文字旋转会牵出一连串问题。
第一,旋转中心的选择。文字不像一个正方形,它的包围盒是不对称的。如果直接绕原点旋转,文字会飞出去。所以需要先计算文字的整体包围盒,找到中心点,然后以这个中心为轴做旋转。这个计算过程涉及到所有字符顶点的遍历和极值统计。
第二,旋转后的可读性。文字旋转到一定角度后,笔画之间的间距在视觉上会发生变化。特别是当文字旋转90度以上时,原本水平的笔画变成垂直,如果线宽没有做相应调整,视觉粗细会不一致。这个问题在轮廓方案里可以通过调整线宽来补偿,但需要额外处理。
第三,旋转与投影的配合。如果场景里有透视投影,文字旋转后会产生近大远小的效果,这时候文字的朝向和相机的相对关系就变得很重要。常见的做法是让文字始终面向相机(Billboard),但那样就不能真正体现“旋转”了。所以这里说的可旋转,通常是指在正交投影下,文字可以在平面内自由旋转。
1.3 整体架构:从字形数据到屏幕像素
整个轮廓文字渲染的流程可以拆成四个阶段:
字形数据获取阶段:从字体文件中提取每个字符的轮廓信息。这一步通常借助FreeType库来完成,它能解析TrueType或OpenType字体,输出字形的轮廓点集。轮廓点集是一系列闭合路径,每条路径由直线段和贝塞尔曲线段组成。
中心线提取阶段:轮廓是字形的外边界,但我们要画的是笔画中心线。这一步需要把闭合轮廓“骨架化”,提取出笔画的中心走向。对于简单字形,可以手动定义中心线;对于复杂字形,需要用算法自动提取。实际项目中,我通常对常用字符手动定义中心线,对生僻字符用简化算法处理。
几何生成阶段:把中心线转换成OpenGL能绘制的图元。最简单的方式是用GL_LINES画线段,但线宽在大多数OpenGL实现里支持不好(很多驱动只支持1像素线宽)。所以更可靠的做法是把每条线段扩展成一个细长的四边形,用两个三角形来绘制。这样线宽可以任意控制,还能做圆角端点。
渲染阶段:设置好模型矩阵(包含旋转和平移)、视图矩阵、投影矩阵,把顶点数据传给GPU,执行绘制。如果需要描边效果,可以用两次绘制:先画粗一点的轮廓色,再画细一点的前景色。
这个架构的核心思想是把文字当成几何图形来处理,而不是当成图像。一旦建立了这个思维模型,旋转、缩放、描边、动画这些需求都变成了常规的几何变换问题。
2. 字形轮廓数据的获取与处理
2.1 FreeType基础用法与关键参数
FreeType是处理字体文件的标配库,它的API设计比较底层,但功能很完整。初始化流程大致如下:
FT_Library library; FT_Init_FreeType(&library); FT_Face face; FT_New_Face(library, "font.ttf", 0, &face); FT_Set_Pixel_Sizes(face, 0, 48); // 设置字形大小这里有个容易踩的坑:FT_Set_Pixel_Sizes的第二个参数是宽度,第三个是高度。如果宽度设为0,表示宽度根据高度自动计算。但有些字体在宽度为0时会出问题,所以更稳妥的做法是同时指定宽高,或者用FT_Set_Char_Size来设置。
加载单个字形用FT_Load_Char:
FT_Load_Char(face, 'A', FT_LOAD_NO_BITMAP | FT_LOAD_NO_HINTING);FT_LOAD_NO_BITMAP告诉FreeType不要生成位图,我们只要轮廓。FT_LOAD_NO_HINTING关闭字形微调,保留原始的轮廓精度。这两个标志对于轮廓提取很关键,如果开了hinting,轮廓点会被对齐到像素网格,旋转后反而不自然。
加载完成后,face->glyph->outline就是轮廓数据。outline.n_contours表示有几条闭合路径,outline.n_points表示总点数,outline.points是点数组,outline.tags标记每个点是直线点还是贝塞尔控制点。
2.2 轮廓点集的解析与中心线提取
FreeType给出的轮廓是字形的外边界,比如字母“O”会给出内外两条闭合路径。我们要画的是笔画中心线,所以需要从轮廓推导出中心线。
对于简单字形,最直接的办法是手动定义中心线。比如字母“I”,中心线就是一条竖直线段;字母“L”,中心线是一条竖线加一条横线。手动定义的好处是中心线质量高,笔画连接处自然。缺点是工作量大,只适合字符集固定的场景。
对于需要自动处理的场景,可以用中轴变换(Medial Axis Transform)来提取中心线。基本思路是:对字形区域做距离变换,然后找距离变换的脊线。这个算法在OpenCV里有现成实现,但计算量比较大,不适合实时生成。
我在实际项目里用的是一种折中方案:基于轮廓点的配对法。对于每条闭合轮廓,找到它的“对面”轮廓,然后在两组点之间做配对,配对点的中点就是中心线上的点。这个方法对笔画粗细均匀的字体效果不错,但对粗细变化大的字体会有偏差。
还有一种更实用的方案:用字体设计软件预先导出中心线。很多字体设计工具(如FontForge)支持导出字形的骨架线,导出的数据可以直接用。这样既保证了质量,又省去了运行时计算的开销。
2.3 贝塞尔曲线的离散化处理
FreeType的轮廓点里,贝塞尔曲线是用控制点表示的。二次贝塞尔曲线有一个控制点,三次贝塞尔曲线有两个控制点。OpenGL不能直接画曲线,需要先离散化成线段。
二次贝塞尔曲线的离散化公式:
// P0: 起点, P1: 控制点, P2: 终点 for (int i = 0; i <= N; i++) { float t = (float)i / N; float x = (1-t)*(1-t)*P0.x + 2*(1-t)*t*P1.x + t*t*P2.x; float y = (1-t)*(1-t)*P0.y + 2*(1-t)*t*P1.y + t*t*P2.y; // 输出点 (x, y) }N的取值很关键。N太小,曲线看起来有棱角;N太大,顶点数暴增,影响性能。我的经验是:对于字号48像素的文字,N取8到12就足够了。如果文字需要放大很多倍,可以按缩放比例动态调整N。
这里有个细节:FreeType的轮廓点里,连续两个点是贝塞尔控制点的情况表示三次贝塞尔曲线。判断逻辑是看tags数组:如果当前点是FT_CURVE_TAG_CONIC,下一个点也是FT_CURVE_TAG_CONIC,那这两个点就是二次贝塞尔的控制点,需要特殊处理。
离散化之后,所有曲线都变成了线段,后续处理就统一了。
3. 从中心线到可渲染几何体
3.1 线段到四边形的扩展算法
OpenGL的glLineWidth在很多驱动上限制很大,所以不能依赖它来画粗线。可靠的做法是把每条线段扩展成一个四边形。
假设线段从点A到点B,线宽为w。扩展步骤如下:
- 计算线段方向向量
d = normalize(B - A) - 计算法向量
n = (-d.y, d.x) - 四个顶点分别为:
A + n * w/2A - n * w/2B - n * w/2B + n * w/2
这样得到一个矩形,用两个三角形绘制即可。
但这个简单做法在折线连接处会有缺口。比如字母“L”的拐角处,两条线段的四边形不能完全覆盖拐角。解决办法有两种:一是用斜接连接(miter join),计算两条线段法向量的角平分线,把顶点延伸到交点;二是用圆角连接,在拐角处加一个圆形的三角形扇。
斜接连接的实现:
// 计算两条线段的法向量 n1, n2 // 角平分线方向 m = normalize(n1 + n2) // 延伸长度 = w/2 / dot(m, n1) // 顶点位置 = 拐角点 + m * 延伸长度当两条线段夹角很小时,延伸长度会变得很大,产生“尖刺”。所以需要设置一个miter limit,超过阈值就退化成斜切连接。
3.2 线宽与旋转角度的视觉补偿
文字旋转后,线宽在视觉上会发生变化。这不是OpenGL的问题,而是人眼的感知特性。当线条从水平变成垂直时,同样的物理宽度看起来会更粗。这是因为人眼对水平线和垂直线的敏感度不同。
补偿的方法很简单:根据旋转角度调整线宽。旋转角度为0或90度时,线宽保持不变;旋转到45度时,线宽可以稍微减小。具体的补偿曲线可以用余弦函数来近似:
float angle = fmod(rotation, 90.0f) * PI / 180.0f; float compensation = 1.0f - 0.1f * sin(2 * angle); float adjustedWidth = baseWidth * compensation;这个补偿系数0.1是我在实际项目中调出来的,不同字体可能需要微调。补偿幅度不宜过大,否则文字看起来会“忽胖忽瘦”。
3.3 顶点缓冲的组织与优化
所有字符的顶点数据可以组织成一个大缓冲区,也可以每个字符一个缓冲区。我的建议是按字符分组,但共享一个VBO。
具体做法是:先遍历所有字符,生成顶点数据,记录每个字符的起始偏移和顶点数。然后把这些数据一次性上传到VBO。绘制的时候,用glDrawArrays的first和count参数来指定绘制哪个字符。
这样做的好处是减少了状态切换。如果每个字符一个VBO,绘制时就要频繁绑定不同的缓冲区,性能会受影响。共享VBO的话,只需要绑定一次,然后连续调用glDrawArrays即可。
顶点格式方面,位置用vec2就够了(文字在平面内),颜色可以用vec4或者用一个uniform统一设置。如果需要每个字符不同颜色,就把颜色也放进顶点属性。
typedef struct { float x, y; // 位置 float r, g, b, a; // 颜色 } Vertex;对于大量文字,还可以考虑用实例化渲染(instancing),把每个字符的变换矩阵作为实例属性。不过对于几百个字符的场景,普通绘制方式已经足够了。
4. 旋转矩阵与坐标变换的实操细节
4.1 旋转中心的计算与包围盒
文字旋转必须绕自己的中心转,否则会跑偏。计算包围盒的步骤如下:
- 遍历所有字符的所有顶点,找到最小和最大的x、y值
- 包围盒中心
center = ((minX + maxX) / 2, (minY + maxY) / 2) - 旋转矩阵以center为原点
在代码里,这个变换可以这样实现:
// 构建变换矩阵 mat4 transform = mat4_identity(); transform = mat4_translate(transform, center.x, center.y, 0); transform = mat4_rotate(transform, rotation, 0, 0, 1); transform = mat4_translate(transform, -center.x, -center.y, 0);这个矩阵的含义是:先把文字平移到原点,旋转,再平移回去。这样旋转中心就是包围盒中心。
如果文字是动态生成的(比如每帧内容都在变),包围盒需要每帧重新计算。对于固定文字,可以缓存包围盒,只在文字内容改变时更新。
4.2 正交投影下的旋转实现
文字渲染通常用正交投影,因为透视投影会让文字产生变形,影响可读性。正交投影矩阵的构建:
mat4 ortho = mat4_ortho(left, right, bottom, top, near, far);其中left、right、bottom、top定义了可视区域。如果窗口大小是800x600,可以设置:
mat4_ortho(0, 800, 0, 600, -1, 1);这样坐标原点在左下角,x向右,y向上。文字的位置就用像素坐标表示,很直观。
旋转矩阵在正交投影下工作得很好,因为正交投影不会引入透视除法,旋转后的顶点坐标直接映射到屏幕空间。
如果需要文字跟随某个3D物体旋转,那就需要把文字的模型矩阵和物体的模型矩阵结合起来。这时候要注意坐标系的转换:文字通常在局部坐标系里定义,需要先变换到世界坐标系,再变换到相机坐标系。
4.3 旋转后的文字位置修正
旋转之后,文字的位置可能会偏移。比如原本文字在屏幕中央,旋转90度后可能跑到一边去了。这是因为旋转矩阵是绕包围盒中心旋转的,但文字的“锚点”可能不是包围盒中心。
解决方法是引入一个锚点概念。锚点可以是文字的左下角、中心、或者任意指定点。旋转时,先把锚点移到原点,旋转,再移回去。
// anchor: 锚点相对于包围盒中心的偏移 vec2 anchorOffset = anchor - center; transform = mat4_translate(transform, anchorOffset.x, anchorOffset.y, 0); transform = mat4_rotate(transform, rotation, 0, 0, 1); transform = mat4_translate(transform, -anchorOffset.x, -anchorOffset.y, 0);这样无论怎么旋转,锚点的位置保持不变。对于需要精确定位的场景(比如文字要贴着一个图标旋转),这个技巧很实用。
5. 常见问题与排查技巧实录
5.1 文字旋转后笔画断裂或重叠
这是最常见的问题,通常有几个原因:
原因一:线段连接处没有处理。如果只是简单地把每条线段扩展成四边形,拐角处会有缺口。解决办法是加上斜接连接或圆角连接。
原因二:浮点数精度问题。当文字旋转角度接近90度时,某些顶点的坐标计算会出现微小误差,导致相邻四边形之间出现缝隙。解决办法是在连接处稍微重叠一点,或者用更高的精度(double)来计算顶点。
原因三:深度测试干扰。如果开启了深度测试,旋转后的文字不同部分可能有不同的深度值,导致部分被遮挡。文字渲染通常应该关闭深度测试,或者把所有文字的深度值设为相同。
排查方法:先用纯色绘制,不加旋转,看文字是否完整。然后逐步加上旋转,观察在哪一步出现问题。还可以用线框模式(glPolygonMode(GL_FRONT_AND_BACK, GL_LINE))来查看几何体的实际形状。
5.2 线宽在不同角度下不一致
这个问题前面提到过,主要是视觉感知的问题。但如果线宽差异非常明显,可能是代码问题:
- 检查法向量计算是否正确。法向量应该是单位向量,如果没归一化,线宽会随线段长度变化。
- 检查旋转矩阵是否正交。如果矩阵有缩放分量,线宽会被缩放。
- 检查是否有非均匀缩放。如果x和y方向的缩放不一致,旋转后线宽会变化。
如果确认代码没问题,那就是视觉感知问题,可以用前面说的角度补偿来缓解。
5.3 性能问题:顶点数过多导致卡顿
轮廓文字的顶点数通常比纹理方案多,因为每条线段都要扩展成四边形。一个复杂的汉字可能有几十条线段,每条线段4个顶点,总共几百个顶点。如果屏幕上有几百个字符,顶点数就上万了。
优化方法:
- 减少曲线离散化的精度。对于小字号文字,N取4到6就够了。
- 合并共线线段。如果两条线段方向相同且相邻,可以合并成一条。
- 使用LOD(Level of Detail)。远处的文字用低精度,近处的用高精度。
- 批量绘制。把所有文字的顶点放在一个VBO里,减少绘制调用。
实测下来,对于1000个字符以内的场景,优化后的轮廓方案在普通集成显卡上也能跑到60帧。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 文字旋转后笔画断裂 | 拐角未处理 | 线框模式查看几何体 | 加斜接或圆角连接 |
| 线宽随角度变化 | 法向量未归一化 | 检查法向量长度 | 归一化法向量 |
| 文字位置偏移 | 旋转中心错误 | 打印包围盒中心 | 修正旋转矩阵 |
| 文字模糊 | 用了纹理方案 | 检查是否加载了位图 | 改用轮廓方案 |
| 性能卡顿 | 顶点数过多 | 统计顶点数量 | 降低离散化精度 |
| 文字倒置 | Y轴方向错误 | 检查投影矩阵 | 调整ortho参数 |
| 颜色不正确 | 顶点属性错位 | 检查顶点格式 | 修正stride和offset |
6. 进阶技巧与扩展方向
6.1 描边效果的实现
描边是轮廓文字的一个自然扩展。实现思路是:先用较粗的线宽和描边颜色绘制一遍,再用较细的线宽和前景色绘制一遍。这样描边就会出现在文字的外围。
// 第一遍:描边 glUniform1f(lineWidthLocation, baseWidth + 2.0f); glUniform4f(colorLocation, 0, 0, 0, 1); // 黑色描边 drawAllCharacters(); // 第二遍:前景 glUniform1f(lineWidthLocation, baseWidth); glUniform4f(colorLocation, 1, 1, 1, 1); // 白色前景 drawAllCharacters();这个方法的缺点是描边会覆盖文字的内部细节。如果文字笔画很细,描边可能会把内部填满。解决办法是控制描边宽度不超过笔画宽度的一半。
另一种方法是几何扩展法:把中心线向两侧扩展时,一侧扩展多一点作为描边,另一侧正常。这样描边和前景是同一个几何体,不会互相覆盖。但实现起来复杂一些。
6.2 文字动画与动态效果
轮廓文字很适合做动画,因为所有效果都可以通过修改顶点或矩阵来实现。
打字机效果:逐渐增加绘制的线段数量,模拟文字被写出来的感觉。实现方法是记录每条线段的起始顶点索引,动画时逐步增加绘制的顶点数。
波浪效果:根据顶点位置和时间计算偏移量,让文字像波浪一样起伏。在顶点着色器里实现:
float offset = sin(position.x * 0.05 + time) * 5.0; gl_Position = projection * view * model * vec4(position.x, position.y + offset, 0, 1);颜色渐变:根据顶点位置计算颜色,让文字有渐变效果。可以在顶点着色器里根据y坐标插值颜色。
6.3 多字体与字符集支持
如果需要支持多种字体,可以把每种字体的中心线数据预先处理好,存成资源文件。运行时根据字体名称加载对应的数据。
字符集方面,ASCII字符可以手动定义中心线,但Unicode字符集太大,必须用自动提取算法。我的做法是:常用字符(ASCII+常用汉字)用预定义数据,生僻字符用自动提取,并缓存结果。
自动提取的算法可以用前面提到的中轴变换,也可以用更简单的轮廓配对法。轮廓配对法的基本思路是:对于每条轮廓,找到距离它最近的另一条轮廓,然后在两条轮廓之间做点配对,配对点的中点就是中心线点。这个方法对笔画粗细均匀的字体效果不错。
6.4 与3D场景的融合
如果文字需要出现在3D场景中,比如标注某个物体的名称,就需要把文字渲染到3D空间里。这时候文字需要有自己的模型矩阵,可能还需要Billboard效果(始终面向相机)。
Billboard的实现方法是在顶点着色器里,用相机的右向量和上向量来构建文字的局部坐标系:
vec3 right = vec3(view[0][0], view[1][0], view[2][0]); vec3 up = vec3(view[0][1], view[1][1], view[2][1]); vec3 worldPos = instancePos + right * position.x + up * position.y; gl_Position = projection * view * vec4(worldPos, 1.0);这样无论相机怎么转,文字始终面向相机。如果不需要Billboard,就直接用普通的模型矩阵变换。
7. 我的实操心得与避坑建议
7.1 关于字体选择
不是所有字体都适合轮廓方案。无衬线字体(如黑体、Arial)效果最好,因为笔画粗细均匀,中心线容易提取。衬线字体(如宋体、Times New Roman)的笔画有粗细变化,中心线提取后描边效果不理想。手写体和艺术字体更麻烦,笔画可能不连续,中心线提取算法基本失效。
如果项目允许选择字体,优先选无衬线字体。如果必须用衬线字体,建议对每个字符手动调整中心线,或者接受一定程度的失真。
7.2 关于坐标系统
OpenGL的坐标系统很容易搞混。我的建议是:统一用像素坐标。投影矩阵设置成ortho(0, width, 0, height, -1, 1),这样所有坐标都是像素单位,直观易懂。文字的包围盒、旋转中心、锚点都用像素坐标表示,不需要额外的单位转换。
如果窗口大小会变化,投影矩阵需要跟着更新。可以在窗口大小改变的回调里重新计算投影矩阵。
7.3 关于性能优化
轮廓文字的性能瓶颈通常在顶点数和绘制调用次数。我的优化顺序是:
- 减少绘制调用:把所有文字合并到一个VBO,用一次
glDrawArrays绘制所有文字。如果文字需要不同的变换,可以用实例化渲染。 - 减少顶点数:降低曲线离散化精度,合并共线线段。
- 使用顶点着色器:把旋转、平移等变换放到顶点着色器里,减少CPU计算。
- 剔除不可见文字:如果文字超出屏幕范围,直接跳过绘制。
实测下来,优化后的轮廓方案在1000个字符的场景下,CPU占用不到5%,GPU占用不到10%。
7.4 关于调试
文字渲染的调试比较麻烦,因为出问题的时候往往看不出具体哪里错了。我的调试流程是:
- 先用纯色绘制,不加旋转。确认文字形状正确。
- 加上旋转,用线框模式。查看几何体是否有断裂或重叠。
- 加上颜色和描边。确认渲染顺序正确。
- 加上动画。确认变换矩阵正确。
如果文字完全看不见,先检查投影矩阵和视图矩阵是否正确。可以用一个简单的三角形测试一下,确认渲染管线没问题。
7.5 一个容易忽略的细节:抗锯齿
轮廓文字如果没有抗锯齿,边缘会有明显的锯齿。开启MSAA(多重采样抗锯齿)可以缓解,但会增加性能开销。另一种方法是在片段着色器里做边缘平滑:根据片段到线段边缘的距离计算alpha值。
float dist = abs(dot(fragPos - lineStart, normal)); float alpha = 1.0 - smoothstep(halfWidth - 1.0, halfWidth, dist);这样可以在不开启MSAA的情况下获得不错的抗锯齿效果。代价是需要传递额外的顶点属性(线段起点和法向量)。
7.6 关于跨平台兼容性
不同平台的OpenGL实现有差异。比如有些移动端GPU不支持glLineWidth大于1,有些桌面GPU不支持顶点纹理采样。轮廓方案因为不依赖这些特性,跨平台兼容性反而比纹理方案好。
但有一点要注意:浮点数精度。移动端GPU通常只支持mediump精度,在计算旋转矩阵时可能会有误差。解决办法是在CPU端用highp计算好矩阵,然后传给GPU。
8. 完整实现流程速览
8.1 从零开始的步骤清单
如果你要从头实现一个轮廓文字渲染器,可以按这个顺序来:
- 集成FreeType,加载字体文件,提取字形轮廓点
- 实现贝塞尔曲线离散化,把轮廓转成线段
- 实现中心线提取(手动定义或自动算法)
- 实现线段到四边形的扩展,处理拐角连接
- 构建VBO,上传顶点数据
- 编写顶点着色器和片段着色器
- 实现旋转矩阵和包围盒计算
- 实现描边效果(两次绘制)
- 优化性能(合并绘制、降低精度)
- 添加抗锯齿和动画效果
每一步都可以独立测试,确认无误后再进行下一步。
8.2 关键参数参考值
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 曲线离散化精度N | 8-12 | 字号48像素时 |
| 线宽 | 2-4像素 | 根据字号调整 |
| 斜接限制 | 2.0 | 超过则退化为斜切 |
| 旋转补偿系数 | 0.1 | 视觉补偿幅度 |
| 抗锯齿过渡宽度 | 1.0像素 | smoothstep范围 |
| 顶点格式 | vec2位置+vec4颜色 | 共24字节 |
这些值是我在实际项目中调出来的,可以作为起点,根据具体需求微调。
8.3 一个最小可运行示例的结构
// 初始化 initFreeType(); loadFont("font.ttf"); extractGlyphs("Hello World"); buildGeometry(); createVBO(); compileShaders(); // 渲染循环 while (!windowShouldClose()) { updateRotation(deltaTime); mat4 mvp = computeMVP(rotation, center); glUniformMatrix4fv(mvpLocation, 1, GL_FALSE, mvp.data); glBindVertexArray(vao); glDrawArrays(GL_TRIANGLES, 0, vertexCount); swapBuffers(); }这个结构很简洁,核心逻辑都在buildGeometry和computeMVP里。把这两个函数写好,整个渲染器就成型了。
轮廓文字渲染这个方案,我从第一次尝试到现在,前后迭代了三四版。最早用纹理方案,后来换成轮廓,再后来优化性能、加描边、加动画。每一步都踩过坑,但也积累了不少经验。如果你正在做类似的东西,希望这些内容能帮你少走点弯路。旋转这个需求看起来简单,但真正做好看了,细节还是挺多的。