ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenGL轮廓文字渲染:可旋转矢量字形实现与优化

OpenGL轮廓文字渲染:可旋转矢量字形实现与优化

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。扩展步骤如下:

  1. 计算线段方向向量d = normalize(B - A)
  2. 计算法向量n = (-d.y, d.x)
  3. 四个顶点分别为:
    • A + n * w/2
    • A - n * w/2
    • B - n * w/2
    • B + 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 旋转中心的计算与包围盒

文字旋转必须绕自己的中心转,否则会跑偏。计算包围盒的步骤如下:

  1. 遍历所有字符的所有顶点,找到最小和最大的x、y值
  2. 包围盒中心center = ((minX + maxX) / 2, (minY + maxY) / 2)
  3. 旋转矩阵以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 关于性能优化

轮廓文字的性能瓶颈通常在顶点数和绘制调用次数。我的优化顺序是:

  1. 减少绘制调用:把所有文字合并到一个VBO,用一次glDrawArrays绘制所有文字。如果文字需要不同的变换,可以用实例化渲染。
  2. 减少顶点数:降低曲线离散化精度,合并共线线段。
  3. 使用顶点着色器:把旋转、平移等变换放到顶点着色器里,减少CPU计算。
  4. 剔除不可见文字:如果文字超出屏幕范围,直接跳过绘制。

实测下来,优化后的轮廓方案在1000个字符的场景下,CPU占用不到5%,GPU占用不到10%。

7.4 关于调试

文字渲染的调试比较麻烦,因为出问题的时候往往看不出具体哪里错了。我的调试流程是:

  1. 先用纯色绘制,不加旋转。确认文字形状正确。
  2. 加上旋转,用线框模式。查看几何体是否有断裂或重叠。
  3. 加上颜色和描边。确认渲染顺序正确。
  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 从零开始的步骤清单

如果你要从头实现一个轮廓文字渲染器,可以按这个顺序来:

  1. 集成FreeType,加载字体文件,提取字形轮廓点
  2. 实现贝塞尔曲线离散化,把轮廓转成线段
  3. 实现中心线提取(手动定义或自动算法)
  4. 实现线段到四边形的扩展,处理拐角连接
  5. 构建VBO,上传顶点数据
  6. 编写顶点着色器和片段着色器
  7. 实现旋转矩阵和包围盒计算
  8. 实现描边效果(两次绘制)
  9. 优化性能(合并绘制、降低精度)
  10. 添加抗锯齿和动画效果

每一步都可以独立测试,确认无误后再进行下一步。

8.2 关键参数参考值

参数推荐值说明
曲线离散化精度N8-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里。把这两个函数写好,整个渲染器就成型了。

轮廓文字渲染这个方案,我从第一次尝试到现在,前后迭代了三四版。最早用纹理方案,后来换成轮廓,再后来优化性能、加描边、加动画。每一步都踩过坑,但也积累了不少经验。如果你正在做类似的东西,希望这些内容能帮你少走点弯路。旋转这个需求看起来简单,但真正做好看了,细节还是挺多的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进