如果您需要呈现的字符数不止一打,那么呈现轮廓线仍然是“不可取”的,因为每个字符所需的顶点数来近似曲率太多了。尽管已经有方法在像素着色器中评估贝塞尔曲线,但这些方法无法轻松地进行反走样处理,而使用距离映射纹理四边形则非常容易实现。而且,在着色器中评估曲线仍然比必要的计算代价更高。
“快速”和“质量”之间最好的折衷方案仍然是使用带有符号距离场纹理的纹理四边形。它比使用普通纹理四边形稍微慢一点,但并不多。另一方面,质量则完全不同。结果真的很惊人,速度也非常快,例如添加发光效果也非常容易。此外,如果需要,该技术可以很好地降级到旧硬件。
有关技术,请参见著名的Valve论文。
这种技术在概念上类似于隐式表面(代谢球等)的工作方式,尽管它不会生成多边形。它完全在像素着色器中运行,并将从纹理采样得到的距离作为距离函数。选择阈值以上的所有内容(通常为0.5)为“内部”,其他所有内容为“外部”。在最简单的情况下,在10年前的非着色器可用硬件上,将alpha测试阈值设置为0.5将完成这个过程(虽然没有特殊效果和反走样)。如果要增加字体的粗度(模拟粗体),则稍微减小阈值即可实现,而无需修改任何一行代码(只需更改“font_weight”统一变量)。对于发光效果,只需将一个阈值以上的所有内容视为“内部”,并将另一个(较小的)阈值以上的所有内容视为“外部,但具有发光效果”,并在两者之间进行LERP。反走样处理方法也类似。
通过使用8位带符号距离值而不是单个位,该技术每个维度将您的纹理映射的有效分辨率增加16倍(使用黑白以外的所有可能的色调,因此使用相同的存储空间我们获得256倍的信息)。但即使您放大到16倍以上,结果仍然看起来相当可接受。长直线最终会变得有点波浪,但没有典型的“块状”采样伪像。
您可以使用几何着色器从点生成四边形(减少总线带宽),但是说实话,收益相对较小。就像在GPG8中描述的那样,对于实例化字符渲染也是如此。仅当您有大量文本需要绘制时,才会摊销实例化的开销。在我看来,这种收益与增加的复杂性和不可降级性之间没有关系。此外,您要么受到常数寄存器数量的限制,要么必须从纹理缓冲区对象中读取,这对于高速缓存一致性来说是非最优的(而意图是进行优化!)。
一个简单的旧式顶点缓冲区只要稍微提前安排上传时间,就可以和任何在过去15年内建造的硬件一样快(甚至更快)。而且,它不限于字体中的任何特定字符数,也不限于要呈现的特定字符数。
如果您确定字体中没有超过256个字符,那么纹理数组可能值得考虑,以类似于从几何着色器的点生成四边形的方式剥离总线带宽。使用数组纹理时,所有四边形的纹理坐标都具有相同的常量s和t坐标,并且仅在r坐标上有所不同,该坐标等于要呈现的字符索引。
但是和其他技术一样,预期的收益很小,而且不兼容以前的硬件。
Jonathan Dummer提供了一个方便的工具来生成距离纹理:
说明页面
更新:
正如最近在“可编程顶点拉动”(D. Rákos,“OpenGL Insights”,pp. 239)中指出的那样,在最新一代GPU上从着色器中以编程方式拉取顶点数据与使用标准固定功能进行相同操作没有显着的额外延迟或开销。
此外,最新一代GPU具有越来越多合理大小的通用L2缓存(例如nvidia Kepler上的1536kiB),因此可以预期从缓冲区纹理中随机偏移量拉取四边形角落时的不连贯访问问题会更少成为问题。
这使得从缓冲区纹理中提取常量数据(例如四边形尺寸)的想法更具吸引力。因此,假设实现可以将PCIe和内存传输以及GPU内存最小化,采用此方法:
- 只上传每个要显示的字符的字符索引(每个索引只有一个),作为顶点着色器的唯一输入,将该索引和
gl_VertexID
传递下去,在几何着色器中将其扩大到4个点,仍然保留字符索引和顶点ID作为唯一属性,并通过变换反馈捕获它。
- 这将很快,因为只有两个输出属性(GS的主要瓶颈),在两个阶段中都接近于“无操作”。
- 绑定一个缓冲纹理,其中包含每个字体字符的纹理四边形顶点相对于基准点的位置(基本上是“字体度量”)。此数据可以通过仅存储底部左侧顶点的偏移量,并编码轴对齐框的宽度和高度(假设是半浮点数,则每个四边形的大小为8个字节的常量缓冲区 – 一个典型的256个字符的字体可以完全放入2kiB的L1缓存中)。
- 设置一个基线uniform。
- 绑定一个带有水平偏移量的缓冲纹理。这些可能甚至可以在GPU上计算,但在CPU上执行这种操作更容易,更有效,因为它是一个严格的顺序操作,而不是完全微不足道的(考虑字距)。此外,它需要另一个反馈传递,这将是另一个同步点。
- 从反馈缓冲区中渲染先前生成的数据,顶点着色器从缓冲对象中提取基准点的水平偏移量和角落顶点的偏移量(使用原始ID和字符索引)。提交的顶点的原始ID现在是我们的“基本ID”(记住GS将顶点转换为四边形)。
这样,理想情况下,可以将所需的顶点带宽降低75%(摊销),但仅能渲染单行。如果要在一个绘制调用中渲染多行,则需要将基线添加到缓冲纹理中,而不是使用uniform(使带宽收益较小)。
但是,即使假设减少了75% - 因为显示“合理”数量的文本所需的顶点数据只有大约50-100kiB(对于GPU或PCIe总线来说实际上是零),我仍然怀疑增加的复杂性和失去向后兼容性真的值得麻烦。将零减少75%仍然只是零。我承认我没有尝试过上述方法,需要进行更多的研究才能作出真正合格的陈述。但是,除非有人能够展示真正惊人的性能差异(使用“普通”数量的文本,而不是数十亿个字符!),否则我的观点仍然是对于顶点数据,简单的旧式顶点缓冲区足以被认为是“最先进解决方案”的一部分。它简单而直接,它工作,并且工作得很好。
已经在上面提到过“
OpenGL Insights”,值得一提的是Stefan Gustavson的章节“2D Shape Rendering by Distance Fields”详细介绍了距离场渲染。
更新2016年:
与此同时,出现了几种额外的技术,旨在消除在极端放大时会变得令人不适的角落圆角伪影。
一种方法简单地使用伪距离场而不是距离场(区别在于距离是到实际轮廓而不是到轮廓或一个虚拟线突出边缘的最短距离)。这样做有些更好,速度相同(使用相同的着色器),使用相同数量的纹理内存。
另一种方法使用三通道纹理中的三个值的中间值详细实现
可在github上获取。这旨在改善以前用来解决问题的and-or hack。质量好,略微,几乎不会注意到,慢,但使用的纹理内存增加了三倍。此外,额外的效果(如光晕)更难以正确地实现。
最后,将构成字符的实际贝塞尔曲线存储,并在片段着色器中评估
已变得可行,性能略低(但不足以成为问题),即使在最高放大倍数下也具有惊人的效果。
使用此技术实时渲染大型PDF的WebGL演示可在
此处找到。