MONO/.NET 中使用 OpenGL/OpenTK 渲染文本的摘要






4.98/5 (16投票s)
简要概述 OpenGL/OpenTK 文本渲染选项,特别是针对 MONO/.NET。
下载 TextPrinter-Test.zip 完整的 MonoDevelop 解决方案以演示
TextPrinter
下载 QuickFont-Test.zip 完整的 MonoDevelop 解决方案以演示
QuickFont
下载 TextureLib-Test.zip 完整的 MonoDevelop 解决方案以演示
TexLib
下载 HandCrafted.zip 完整的 MonoDevelop 解决方案以演示
TextRenderer
Ver. 1
下载 TextRenderer-Test.zip 完整的 MonoDevelop 解决方案以演示
TextRenderer
Ver. 2
下载 FreeType-Test.zip 完整的 MonoDevelop 解决方案以演示
FtFont
Ver. 1 和 Ver. 2
下载 FreeTypeDynamic-Test.zip 完整的 MonoDevelop 解决方案以演示
FtFont
Ver. 3
引言
本文旨在帮助快速了解 OpenGL/OpenTK 的文本渲染选项,特别是针对 MONO/.NET 编程语言。我希望分享我的发现,并帮助正在寻找适合其需求的解决方案的程序员。
背景
有两种基本方法
- 传统方法:使用字体字形,利用 CPU 将文本渲染到位图,将位图作为纹理传输到 GPU 并将其混合到场景中。
- 创新方法:在 GPU 上将字体字形渲染为快速计算的轮廓(弧线和线段而不是样条和多项式段),在 GPU 上根据像素到轮廓距离计算渲染纹理,并将其混合到场景中。
让我们首先简要讨论一下创新方法:这种方法一方面需要强大的 GPU 和大量的纹理缓冲区内存,但另一方面,它显著减轻了 CPU 的负担。目前,已知只有一个实现:GLyphy。依我看来,这绝对是未来的方法。之所以说是未来,是因为 GLyphy 在 Mesa、几乎所有视频驱动程序以及其测试过的像素着色器中都检测到各种实现错误。在它被广泛使用之前,这些错误必须得到修复。
现在我们回到传统方法:有三种做法
1.a. | 将要显示的文本渲染成(最终纹理)位图。然后将此位图作为纹理混合到 GL 窗口的场景中。 |
1.b. | 将字体的字形渲染到(中间纹理)位图,从该位图中提取字形,并将其组合成一个表示要显示的文本的新(最终纹理)位图。然后将此位图作为纹理混合到 GL 窗口的场景中。 |
1.c. | 将位图字体加载到(中间纹理)位图,从该位图中提取字形,并将其组合成一个表示要显示的文本的新(最终纹理)位图。然后将此位图作为纹理混合到 GL 窗口的场景中。 |
做法 1.a.(文本到最终纹理位图)需要用于渲染要显示的文本的图形上下文的 DrawString()
和 MeasureString()
方法。
优点: Windows GDI 或 X11 字体服务器提供了所有需要的功能。可以使用任何系统字体。
缺点: 质量取决于所提供的 DrawString()
和 MeasureString()
方法的实现。
描述: 在 Windows 上,System.Drawing.Graphics
类实现产生了出色的输出质量。在 X11 上,Mono 对 System.Drawing.Graphics
类的实现产生了范围广泛的输出质量
- 任何均匀背景上的任何文本颜色都会产生出色的结果。
- 渐变背景上的任何黑色/灰色/白色文本都会产生良好的结果。
- 彩色文本,尤其是多色文本,需要大量调整才能产生可接受的结果。
在 X11 上,Mono 的 Pango 或 Cairo 的 Pango 调用封装可能是一个很好的替代方案,而不是 Mono 的 System.Drawing.Graphics
命名空间(Windows GDI 复制品)。Cairo 提供了 Cairo.Context.ShowText()
和 Cairo.Context.TextExtents()
作为 System.Drawing.Graphics.DrawString()
和 System.Drawing.Graphics.MeasureString()
的等效功能。但我没有找到“即用型”代码,可以实现在 OpenTK 上下文中的所需功能。
做法 1.b.(字形到中间纹理位图,摘录到最终纹理位图)有点“重复造轮子”。因为将字体的字形渲染到(中间)位图与 Windows GDI 或 X11 字体服务器已经做的事情是相同的。
优点: 对整个文本渲染链(字形、纹理、混合)具有绝对控制。可以实现任何质量和任何效果。可以使用任何系统字体。
缺点: 创建和管理(中间)字体位图需要大量精力。提取字形纹理摘录并组合成字符串也需要大量精力。程序初始化需要字体位图初始化并消耗运行时。
描述: 这种做法也需要 DrawString()
和 MeasureString()
方法,但生成的字体位图可以进行后处理以实现特定的质量或效果。Mono System.Drawing.Graphics
类实现中的缺点可以得到弥补。
在 X11 上,应使用基于 FreeType 的文本绘制,而不是 Mono 的 System.Drawing.Graphics
命名空间(Windows GDI 复制品)。
做法 1.c.(位图字体到中间纹理位图,摘录到最终纹理位图)类似于做法 1.b,但它不提供与 Windows GDI 或 X11 字体服务器相同的字体——它提供来自特定位图字体文件的字体。这种做法通常用于游戏。
优点: 对整个文本渲染链(字形、纹理、混合)具有绝对控制。可以实现任何质量和任何效果。
缺点: 创建和管理字体位图需要大量精力。提取字形纹理摘录并组合成字符串也需要大量精力。需要特定的字体文件。程序初始化需要字体位图初始化,但比 1.b 快得多。
描述: 字体位图的创建可以与其使用完全分离(按时间、按资源、按位置)。人工或纹理字体易于实现。大多数字体是等宽的,但比例字体是可能的——除了字体文件提供的位图之外,它们还需要字形宽度。
使用代码
我准备了四个示例解决方案,涵盖了做法 1.a.、1.b. 和 1.c.,所有这些解决方案都在 MonoDevelop 5.0.1 上,使用 Mono 3.8.0 和 .Net 4.0。OpenTK 库是程序集版本 1.1.0.0,Mesa 库是 10.3.7
- 做法 1.a.(文本到最终纹理位图)TextPrinter-Test(利用
OpenTK.Graphics.TextPrinter
) - 做法 1.b.(字形到中间纹理位图,摘录到最终纹理位图)QuickFont-Test(利用 OpenTK 的
QuickFont
类) - 做法 1.c.(位图字体到中间纹理位图,摘录到最终纹理位图)TextureLib-Test(利用 TextureLib 的 OpenTK
TextureFont
类) - 质量比较 HandCrafted-Test(做法 1.a. 和做法 1.b.)
文章版本 2.0 更新
我已将 MonoDevelop 5.0.1 更新到 MonoDevelop 5.10 以解决频繁的调试器崩溃问题。
我找到了“缺失的部分”,可以在文章 使用 OpenGL 渲染 FreeType/2 和 使用 OpenGL 渲染 AltNETType(= .NET FreeType 端口) 中使用 FreeType 代替 Mono 的 GDI 实现(System.Drawing.Graphics
类)。这些文章引导我找到了 FtFont
类,它结合了 OpenGL 3.3+ 文本...、freetype-gl 和 FreeType 字体 中的思想。
我添加了三个进一步的示例应用程序,涵盖了做法 1.a 和 1.b
- 做法 1.a.(文本到最终纹理位图)TextRenderer-Test(利用
System.Drawing.Graphics
类) - 做法 1.b.(字形到中间纹理位图,摘录到最终纹理位图)FreeTypeGlyphWise-Test(利用 FreeType 字体类
FtFont
绘制位图,并将位图作为纹理字形逐个映射) - 做法 1.b.(字形到中间纹理位图,摘录到最终纹理位图)FreeTypeLineWise-Test(利用 FreeType 字体类
FtFont
绘制位图,并将位图作为完整字符串的纹理映射)
文章版本 5.0 更新
我找到了解决 SFML 代码中“unicode 字符”问题(例如“¬”而不是“€”)的“缺失部分”。该库将字形动态添加到中间纹理位图。我建议阅读 SFML-2.5.0\src\SFML\Graphics\Font.cpp
、SFML-2.5.0\src\SFML\Graphics\Texture.cpp
和 SFML-2.5.0\src\SFML\Graphics\Text.cpp
。对于此解决方案,我必须实现
- 一种纹理复制算法,当需要为新字形动态追加时(参见技巧 将 GL 纹理复制到另一个 GL 纹理或 GL 像素缓冲区,以及从 GL 像素缓冲区复制 和示例应用程序的
FtTexture
类),保留中间纹理位图中现有字形位图的情况, - 一个新的“字符代码到字形”映射,提供动态字形追加(参见示例应用程序的
FtFontPage
和FtGlyphTable
类),以及 - 一个 GL 命令管道,将字符串绘制分为两个部分
- GL 命令的准备
- GL 命令的执行(参见示例应用程序的
GlCommandSequence
类)。
因为如果中间纹理在需要添加新字形位图的情况下需要扩大,则动态字形追加在字符串绘制期间无法工作。此外,命令管道提供了缓冲 GL 命令的优势,并避免了将来重新计算字形纹理位图摘录。
新的示例应用程序 FreeTypeDynamic-Test 实现了所有这些技术。此示例应用程序基于 FreeTypeLineWise-Test。新的示例应用程序
- 解决了“unicode 字符”问题,例如“¬”而不是“€”,
- 实现了字距调整(可选,因为字距调整会使速度降低约一半),并且
- 产生相同的出色质量。无偏色像素,无残留。
做法 1.a. 示例程序 - TextPrinter 和 TextRenderer
TextPrinter-Test 示例基于
OpenTK.Compatibility.dll
中的已过时 TextPrinter
。过时并不意味着代码完全过时。相反,该代码目前缺乏维护者以使其与 OpenTK 开发进展保持一致。使用 TextPrinter
的输出质量非常出色。
下图显示了使用 TextPrinter
和七种不同颜色字体的示例输出。质量非常出色。无偏色像素,无残留。
OpenTK 社区建议编写自己的文本打印机,而不是使用 TextPrinter
。但 TextPrinter
仍然存在,产生非常好的输出质量,并且是开源的(即使在遥远的未来它将从 OpenTK.Compatibility.dll
中删除,也可以复制和使用)。
示例程序 HandCrafted-Test 将更深入地探讨“编写自己的文本打印机”这一方面。
文章版本 2.0 更新
TextRenderer-Test 示例基于 HandCrafted-Test 示例的
TextRenderer
技术,但产生与 TextPrinter-Test 示例相同的输出。输出质量与已过时的 TextPrinter
类相当。
下图显示了使用 TextRenderer
和七种不同颜色字体的示例输出。质量非常出色。无偏色像素,无残留。
要将 TextRenderer
作为已过时 TextPrinter
的替代方案,需要付出大量努力来加速文本渲染。
示例程序 FreeTypeGlyphWise-Test 和 FreeTypeLineWise-Test 也将更深入地探讨“编写自己的文本打印机”这一方面,并且比 TextRenderer
技术更快。
做法 1.b. 示例程序 - QuickFont、FreeTypeGlyphWise 和 ~LineWise
QuickFont-Test 示例基于 OpenTK QuickFont 代码。使用
QuickFont
的输出质量可以从差到非常好——取决于字体。有些字体会产生残留,这似乎是因为 QuickFont
在字形之间使用的空间太少。
下图显示了使用 QuickFont
和七种不同颜色字体的示例输出。质量各不相同。没有偏色像素,但在 DroidSerif-Bold、DejaVu Serif 和 luximr 中存在残留。
文章版本 2.0 更新
在处理示例解决方案 FreeTypeLineWise-Test 时,我也遇到了残留问题,并用中间位图中的两条额外扫描线(字形上方一条,下方一条)解决了这个问题。我认为从中间位图中提取字形纹理摘录时,使用 glTexCoord2() 在 0.0 ... 1.0 的坐标范围内进行,精度不足,导致了这些问题。也许在中间位图中添加额外的扫描线也能解决 QuickFont
的问题。
FreeTypeGlyphWise-Test 示例基于 使用 OpenGL 渲染 FreeType/2 文章中的代码。使用
FtFont
类的第一个版本的输出质量非常出色。
下图显示了使用 FtFont
类第一个版本和七种不同颜色字体的示例输出。质量非常出色。无偏色像素,无残留。
FreeTypeLineWise-Test 示例改进了
FtFont
类以支持字符串渲染而不是字符渲染(即将文本字形纹理逐个字形映射到视口)。使用 FtFont
类第二个版本的输出质量也非常好。
下图显示了使用 FtFont
类第二个版本和七种不同颜色字体的示例输出。质量非常出色。无偏色像素,无残留。
文章版本 5.0 更新
FreeTypeDynamic-Test 示例改进了 FreeTypeLineWise-Test 示例和
FtFont
类,以支持字形纹理的动态字形追加。使用 FtFont
类第三个版本的输出质量也非常好。
下图显示了使用 FtFont
类第三个版本和七种不同颜色字体的示例输出。质量非常出色。无偏色像素,无残留。字形定位已重新设计,现在文本输出符合字体指标。
如您所见,“unicode 字符”问题(例如“¬”而不是“€”)已解决。
字距调整已关闭(因为字距调整会使速度降低约一半)。
收缩已开启(这将字符间距减少约 1/12 字符前进量)。
字距调整和收缩是
public Size DrawString (string text, uint characterSizeInPPEm, bool bold, int startX, int startY,
bool applyKerning = false, bool shrink = false)
方法的新参数。
做法 1.c. 示例程序 - TextureLib
TextureLib-Test 示例基于 OpenTK TexLib 代码。使用 TexLib 的输出质量可以非常好——取决于字体位图文件的质量。
下图显示了使用 TexLib
和七种不同字体的示例输出。质量各不相同。提供的位图字体 big-outline 的升部和降部被截断,其他字体位图是快速粗糙创建的,仅显示黑色文本。获取现成的高质量字体位图文件似乎是一个问题。TexLib
限制为 16 x 16 字形是一个限制。
质量比较示例程序 - HandCrafted
HandCrafted-Test 示例比较了
TextRenderer
类、已过时的 TextPrinter
类和 QuickFont
类的最佳输出质量。
下图显示了 TextRenderer
(第一行红色文本)与 TextPrinter
(第二行红色文本)和 QuickFont
(第三行红色文本)的示例输出比较。质量非常出色。无偏色像素,无残留。
下一张图片显示了 600% 缩放的细节,以比较新的 TextRenderer
类输出(上方红色字符串)与已过时的 TextPrinter
类输出(下方红色字符串)。
顺便说一句:绿色字符串也是 TextRenderer
类的输出。
结论
一个选择是改进 TextRenderer
类,使其在未来方便、快速且文档完善,因为它代码量更少,并且产生与 TextPrinter
相同的质量。尽管如此,TextPrinter
也是一个不错的选择。
另一个选择是开发一个 FreeType 字体类,以避免 Mono 实现 System.Drawing.Graphics
命名空间(Windows GDI 复制品)的缺点。
文章版本 2.0 更新
FreeType FtFont
的实现已成功且比 TextRenderer
实现快得多。现在我更倾向于继续使用 FtFont
方法,并修复“unicode 字符”问题,例如“¬”而不是“€”。
文章版本 4.0 更新
除了 FtFont
类,还有其他替代方案
文章版本 5.0 更新
我已经修复了“unicode 字符”问题,例如“¬”而不是“€”。我还添加了字距调整(可选,因为字距调整会将速度降低约一半)。我绝对建议继续使用 FreeType FtFont
实现。
性能问题
文章版本 2.0[1] 和 5.0[2] 更新
这些性能数据是使用 VMware® Player 7.1.2 build-2780323 虚拟机和 i7-5600U CPU 的两个核心测量的。
示例 | 实践 | 质量 1 2 3 | 性能 低 CPU 负载 | 性能 高 CPU 负载 | |
---|---|---|---|---|---|
TextPrinter-Test | 1.a. | 文本到最终纹理位图 | A A A | 190 帧/秒 | 45 帧/秒 |
Quick-Font-Test | 1.b. | 字形到中间纹理位图, 摘录到最终纹理位图 | A B A | 200 帧/秒 | 50 帧/秒 |
TextureLib-Test | 1.c. | 位图字体到中间纹理 位图,摘录到最终纹理位图 | A A C | 380 帧/秒 | 185 帧/秒 |
TextRenderer-Test[1] | 1.a. | 文本到最终纹理位图 | A A A | 40 帧/秒 | 5 帧/秒 |
FreeTypeGlyphWise-Test[1] | 1.b. | 字形到中间纹理位图, 摘录到最终纹理位图 | A A B | 45 帧/秒 | 9 帧/秒 |
FreeTypeLineWise-Test[1] | 1.b. | 字形到中间纹理位图, 摘录到最终纹理位图 | A A B | 400 帧/秒 | 175 帧/秒 |
FreeTypeDynamic-Test[2] | 1.b. | 字形到中间纹理位图, 摘录到最终纹理位图 | A A A | 320 帧/秒 | 150 帧/秒 |
质量等级为
- 1 偏色像素(在多色背景上):A = 无
- 2 残留物(来自重叠纹理位图):A = 无,B = 某些字体上有残留,C = 始终有残留
- 3 其他:A = 无错误字形,B = 某些 Unicode 字符有错误字形,C = 某些 Unicode 字符有错误字形且某些上升/下降部分被截断
关注点
HandCrafted-Test 示例
使 TextRenderer
类正常工作非常棘手——这些是引导我找到最终解决方案的发现
- 对字体位图使用
System.Drawing.Imaging.PixelFormat.Format32bppArgb
像素格式。- 请注意所有位图的位操作,位图位以 BGRA 顺序存储。
- 字体位图背景使用
Color.FromArgb (0, 0, 0, 0)
,而不是Color.Black
(即 255, 0, 0, 0)。- 如果可以直接使用前景颜色的 RGB 分量值,则颜色计算很容易,因为它们已经表示从背景颜色到前景颜色的边距。
- 黑色不会在 alpha 混合中使前景颜色失真,无论是绝对(亮度)还是相对(色调)。
- 使用自定义
Clear()
方法填充字体位图的背景。System.Drawing.Graphics
方法Clear()
不适用,因为它在 alpha 字节为 0 时不设置颜色。System.Drawing.Graphics
方法FillRectangle()
不适用,因为它会与现有颜色进行 alpha 混合。
- 设计一个
PostprocessForeground()
方法来调整字体位图像素的颜色和 alpha 值,以防止在混合过程中出现背景阴影。- 对于每个 RGB 颜色分量:应用整个目标颜色 RGB 分量以防止颜色失真,并相应地按比例增亮其他 RGB 颜色分量边距(从背景 RGB 颜色分量到前景 RGB 颜色分量)。
- 对于 alpha 字节:应用 RGB 颜色分量边距的最高值。
// Calculate the margin from the background RGB color components
// to the foreground RGB color components.
int deltaB = Math.Abs(bitmapData[index ] - targetColor.B);
int deltaG = Math.Abs(bitmapData[index + 1] - targetColor.G);
int deltaR = Math.Abs(bitmapData[index + 2] - targetColor.R);
// Determine the highest RGB color component margin.
int deltaM = Math.Max(deltaB, Math.Max (deltaG, deltaR));
// Apply the entire target color RGB component to prevent color falsification
// and the respectively other RGB color component margins proportional to brighten up.
bitmapData[index ] = (byte)Math.Min(255, targetColor.B + deltaR / 3 + deltaG / 3);
bitmapData[index + 1] = (byte)Math.Min(255, targetColor.G + deltaR / 3 + deltaB / 3);
bitmapData[index + 2] = (byte)Math.Min(255, targetColor.R + deltaG / 3 + deltaB / 3);
// Now we have exactly the target color or the target color proportional to
// brighten up and can apply the highest RGB color component margin to the alpha byte.
bitmapData[index + 3] = (byte)Math.Min(255, 255 - deltaM);
- 根据 alpha 值混合字体位图和场景。
- 设置
GL.Color4 (1f, 1f, 1f, 1f);
- 设置
GL.Enable (EnableCap.Blend );
- 设置
GL.BlendFunc (BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);
- 设置
文章版本 2.0 / FreeTypeLineWise-Test 示例更新
提供 FreeType 替代 System.Drawing.Graphics
类的初步步骤已成功完成。目前缺少的是
- 解决“某些 unicode 字符字形错误”问题(例如“¬”而不是“€”)的修复
- 一个快速的
MeasureString()
方法。 - 缩放、旋转、字距调整。
- 高度复杂的便捷方法,例如计算自动换行等。
文章版本 3.0 / 文本光栅化曝光更新
有一篇非常有趣的文章 文本光栅化曝光 讨论了文本渲染的细节,该文章是为 Anti-Grain Geometry (AGG) 库编写的。讨论的文本渲染细节主要包括:
- 字形微调(将字形轮廓的插值点捕捉到像素网格以实现锐利的笔触外观,但接受不均匀的笔触粗细——尤其是在低屏幕分辨率下)
- 亚像素定位(在不同位置为相同大小和粗细的相同字形创建不同的像素集,以提高字形位置精度——这牺牲了锐利笔触外观的方面,以获得字形之间和谐间距的方面)
- 亚像素渲染(在 LCD 显示器上使用一个像素的 RGB 亚像素,将字形轮廓在 x 方向的精度提高到 1/3 像素大小——这适用于所有按 RGB 亚像素并排放置的显示器类型,并牺牲了色度/色彩保真度的方面,以获得亮度/亮度保真度的方面)
- 字距调整(减少/增加两个特定字形之间的默认间距到单个间距,如果一个或两个字形留下/占据可以/不能用于实现视觉上令人愉悦的结果的空间)
- 伽马校正(防止圆形和斜向笔画看起来比水平/垂直笔画更粗)
即使这篇文章很旧(2007 年 7 月),并且 ClearType 自从通过 DirectWrite(Windows 7)改进后表现得更好,它仍然显示了文本渲染的复杂性。
随后的图像显示了具有色彩保真度的灰度抗锯齿文本(左侧)和具有亮度保真度的亚像素定位文本(右侧),放大到 400%。两个文本均使用 WPF (.NET 4.5) 渲染,并且在不同位置具有不同的像素集。
虽然灰度抗锯齿文本可能会显示锯齿状边缘,尤其是在高对比度和小字母的情况下。
另一方面,亚像素定位文本在彩色背景上可能会显得失真,尤其是在渐变或变化的背景颜色情况下。
历史
这是 2015 年 11 月 21 日的第一个版本。
第二个版本是 2016 年 1 月 13 日(改进了 TextRenderer 和新的 FreeType 示例 FreeTypeGlyphWise 和 FreeTypeLineWise;文本改进)。
第三个版本是 2016 年 7 月 4 日(文本修复和改进;附加链接)。
第四个版本是 2018 年 10 月 3 日(文本修复和改进;附加链接)。
第五个版本是 2018 年 10 月 27 日(新的 FreeTypeDanamic 和 FreeType 示例;文本改进)。