使用 WebGL 渲染文本的技术

Daniel Velasquez - 2019 年 12 月 6 日

WebGL 中的规则是,任何看起来很简单的东西实际上都相当复杂。 绘制线条调试着色器,文本渲染… 它们在 WebGL 中都很难做好。

这很奇怪吗?WebGL 没有内置的文本渲染函数。尽管文本似乎是最基本的功能之一。当涉及到实际渲染它时,事情就变得复杂起来。您如何考虑每种语言的大量字形?如何处理等宽或比例字体?当文本需要从上到下、从左到右或从右到左渲染时该怎么办?数学公式、图表、乐谱?

突然间,为什么文本渲染在像 WebGL 这样的**低级**图形 API 中没有位置开始变得有意义。文本渲染是一个复杂的问题,有很多细微差别。如果我们想渲染文本,我们需要发挥创意。幸运的是,许多聪明的人已经想出了一种广泛的技术来满足我们所有 WebGL 文本的需要。

在本文中,我们将学习其中一些技术,包括如何生成它们需要的资产以及如何在ThreeJS(一个包含 WebGL 渲染器的 JavaScript 3D 库)中使用它们。作为奖励,每种技术都将有一个演示使用案例。

目录


关于 WebGL 之外文本的快速说明

虽然本文是关于 WebGL 内部文本的,但您应该首先考虑的是,是否可以使用HMTL 文本画布叠加在 WebGL 画布之上。文本作为叠加层无法被 3D 几何体遮挡,但您可以开箱即用地获得样式和可访问性。在很多情况下,这都是您需要的。

字体几何

渲染文本的常用方法之一是使用一系列三角形来构建字形,就像普通模型一样。毕竟,渲染点、线和三角形是 WebGL 的强项。

在创建字符串时,每个字形都是通过从三角形点的字体文件中读取三角形来创建的。从那里,您可以挤出字形使其成为 3D,或者通过矩阵运算缩放字形。

常规字体表示(左)和字体几何(右)

字体几何最适合少量文本。这是因为每个字形包含许多三角形,这会导致绘制变得有问题。

使用字体几何渲染您正在阅读的这段确切的段落会创建 185,084 个三角形和 555,252 个顶点。这只有 259 个字母。使用字体几何编写整篇文章,您的电脑风扇也可能变成飞机涡轮机

尽管三角形的数量会因三角剖分的精度和使用的字体而异,但渲染大量文本在使用字体几何时可能会始终是一个瓶颈。

如何从字体文件创建字体几何

如果像选择您想要的字体并渲染文本一样容易。我不会写这篇文章。常规字体格式使用贝塞尔曲线定义字形。另一方面,在 WebGL 中绘制这些曲线在 CPU 上非常昂贵,而且也很复杂。如果我们想渲染文本,我们需要从贝塞尔曲线创建三角形(三角剖分)。

我发现了一些三角剖分方法,但它们绝非完美,可能不适用于每种字体。但至少它们可以帮助您开始对自己的字体进行三角剖分。

方法 1:自动且轻松

如果您使用的是 ThreeJS,您可以将字体类型传递给FaceType.js,以从字体文件读取参数曲线并将其放入 .json 文件中。ThreeJS 中的字体几何功能会为您处理三角剖分点。

const geometry = new THREE.FontGeometry("Hello There", {font: font, size: 80})

或者,如果您没有使用 ThreeJS 并且不需要实时三角剖分。您可以使用 ThreeJS 为您三角剖分字体,从而避免手动过程的痛苦。然后,您可以从几何体中提取顶点和索引,并在您选择的 WebGL 应用程序中加载它们。

方法 2:手动且痛苦

手动三角剖分字体文件的方法非常复杂,至少一开始是这样。仅仅要详细解释它就需要一整篇文章。也就是说,我们将快速回顾一下我从StackOverflow中获得的基本实现的步骤。

查看 CodePen 上 Daniel Velasquez (@Anemolo) 的笔
三角剖分字体

CodePen 上。

该实现基本上可以分解如下

  1. 将 OpenType.js 和 Earcut.js 添加到您的项目中。
  2. 使用OpenType.js从您的 .tff 字体文件中获取贝塞尔曲线。
  3. 将贝塞尔曲线转换为封闭形状,并按面积降序排序。
  4. 通过弄清楚哪些形状在其他形状内部来确定孔的索引。
  5. 将所有点发送到Earcut,并将孔索引作为第二个参数。
  6. 使用 Earcut 的结果作为几何体的索引。
  7. 呼气。

是的,很多。而且此实现可能不适用于所有字体类型。尽管如此,它可以帮助您入门。

在 ThreeJS 中使用文本几何

谢天谢地,ThreeJS 开箱即用地支持文本几何。给它一个您喜欢的字体的贝塞尔曲线的 .json,ThreeJS 会在运行时为您处理三角剖分顶点。

var loader = new THREE.FontLoader();
var font;
var text = "Hello World"
var loader = new THREE.FontLoader();
loader.load('fonts/helvetiker_regular.typeface.json', function (helvetiker) {
  font = helvetiker;
  var geometry = new THREE.TextGeometry(text, {
    font: font,
    size: 80,
    height: 5,
  });
}

优点

  • 它可以轻松挤出以创建 3D 字符串。
  • 使用矩阵运算可以更容易地进行缩放。
  • 它提供了根据使用的三角形数量而定的高品质。

缺点

  • 由于三角形数量众多,这对于大量文本来说扩展性不佳。由于每个字符都由许多三角形定义,即使渲染像“Hello World”这样简短的东西也会产生 7,396 个三角形和 22,188 个顶点。
  • 这不利于常见的文本效果。
  • 抗锯齿取决于您的后期处理锯齿或浏览器默认设置。
  • 缩放过大可能会显示三角形。

演示:淡出字母

在以下演示中,我利用了使用字体几何体创建 3D 文本的简便性。在顶点着色器中,随着时间的推移,挤出量会增加和减少。将它与标准材质配对,您就可以看到这些幽灵般的字母从虚空中进进出出。

注意,在字母数量很少的情况下,三角形的数量已经达到了数万个!

文本(和画布)纹理

制作文本纹理可能是最简单和最古老的用 WebGL 绘制文本的方式。打开 Photoshop 或其他栅格图形编辑器,绘制带有文本的图像,然后将这些纹理渲染到四边形上,您就完成了!

或者,您可以使用画布在运行时按需创建纹理。您也可以将画布渲染为纹理到四边形上。

除了是最简单的技术外,文本纹理和画布纹理的优势在于每个纹理或每段文本只需要一个四边形。如果您真的想,您可以在单个纹理上写下整个不列颠百科全书。这样,您只需渲染一个四边形、六个顶点和两个面。当然,您会按比例进行操作,但想法仍然存在:您可以将多个字形批处理到同一个四边形中。文本和画布纹理在缩放方面都遇到了问题,尤其是在处理大量文本时。

对于文本纹理,用户必须下载构成文本的所有纹理,然后将它们保存在内存中。对于画布纹理,用户无需下载任何东西,但现在用户的计算机必须在运行时执行所有光栅化操作,并且您需要跟踪每个单词在画布中的位置。此外,更新大型画布可能会非常慢。

如何创建和使用文本纹理

文本纹理没有任何花哨的操作。打开您最喜欢的栅格图形编辑器,在画布上绘制一些文本,然后将其导出为图像。然后您可以将其加载为纹理,并将其映射到平面。

// Load texture
let texture = ;
const geometry = new THREE.PlaneBufferGeometry();
const material new THREE.MeshBasicMaterial({map: texture});
this.scene.add(new Mesh(geometry,material));

如果您的 WebGL 应用程序有很多文本,下载一个巨大的文本精灵表可能不是理想的选择,尤其是对于连接速度较慢的用户而言。为了避免下载时间,您可以使用离屏画布按需光栅化内容,然后将该画布采样为纹理。

让我们用性能来换取下载时间,因为光栅化大量文本需要不止一瞬间。

function createTextCanvas(string, parameters = {}){
    
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    
    // Prepare the font to be able to measure
    let fontSize = parameters.fontSize || 56;
    ctx.font = `${fontSize}px monospace`;
    
    const textMetrics = ctx.measureText(text);
    
    let width = textMetrics.width;
    let height = fontSize;
    
    // Resize canvas to match text size 
    canvas.width = width;
    canvas.height = height;
    canvas.style.width = width + "px";
    canvas.style.height = height + "px";
    
    // Re-apply font since canvas is resized.
    ctx.font = `${fontSize}px monospace`;
    ctx.textAlign = parameters.align || "center" ;
    ctx.textBaseline = parameters.baseline || "middle";
    
    // Make the canvas transparent for simplicity
    ctx.fillStyle = "transparent";
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    
    ctx.fillStyle = parameters.color || "white";
    ctx.fillText(text, width / 2, height / 2);
    
    return canvas;
}

let texture = new THREE.Texture(createTextCanvas("This is text"));

现在您可以将纹理用于平面,就像之前的代码片段一样。或者您可以创建一个精灵

或者,您可以使用更高效的库来创建纹理或精灵,例如three-text2dthree-spritetext。如果您想要多行文本,您应该查看这个很棒的教程

优点

  • 这为静态文本提供了出色的 1:1 质量。
  • 顶点/面数很少。每个字符串可以使用少至六个顶点和两个面。
  • 在四边形上实现纹理很容易。
  • 使用画布或图形编辑器添加效果(例如边框和光晕)非常简单。
  • 画布使创建多行文本变得很容易。

缺点

  • 如果在光栅化后缩放、旋转或变换,则看起来模糊。
  • 在非视网膜设备上,文本看起来很粗糙。
  • 您必须光栅化所有使用的字符串。很多字符串意味着很多数据要下载。
  • 如果不断更新画布,使用画布按需光栅化可能会很慢。

演示:画布纹理

画布纹理非常适合处理少量且不经常更改的文本。因此我创建了一个简单的文本墙,其中四边形重复使用相同的纹理。

位图字体

字体几何体和文本纹理在处理大量文本时都遇到相同的问题。每段文本有一百万个顶点效率极低,而且为每段文本创建一个纹理实际上无法扩展。

位图字体通过将所有独特的字形光栅化到单个纹理中来解决这个问题,该纹理称为纹理图集。这意味着您可以在运行时通过为每个字形创建一个四边形并采样纹理图集的区域来组装任何给定的字符串。

这意味着用户只需为所有文本下载和使用一个纹理。这也意味着您只需渲染每个字形少至一个四边形。

位图字体采样的可视化

渲染整篇文章大约需要 117,272 个顶点和 58,636 个三角形。与仅渲染一段的字体几何体相比,效率提高了 3.1 倍。这是一个巨大的改进!

由于位图字体将字形光栅化为纹理,因此它们会遇到与普通图像相同的问题。放大或缩小时,您会开始看到像素化和模糊的混乱。如果您想要不同大小的文本,您应该发送一个包含特定大小字形的辅助位图。或者您可以使用符号距离场 (SDF),我们将在下一节中介绍。

如何创建位图字体

有很多工具可以生成位图。以下是一些更相关的选项。

  • Angelcode 的 bmfont – 这是由位图格式的创建者创建的。
  • Hiero – 这是一个 Java 开源工具。它与 Anglecode 的 bmfont 非常相似,但它允许您添加文本效果。
  • Glyphs Designer – 这是一个付费的 MacOS 应用程序。
  • ShoeBox – 这是一个用于处理精灵(包括位图字体)的工具。

我们将使用Anglecode 的 bmfont作为我们的示例,因为我认为它是最容易上手的。在本节的底部,您可以找到其他工具,如果您认为它缺少您正在寻找的功能。

当您打开应用程序时,您会看到一个充满字母的屏幕,您可以选择使用这些字母。它的好处是您可以只获取所需的字形,而无需发送希腊符号。

应用程序的侧边栏允许您选择和选择字形组。

BmFont 应用程序

准备导出?转到选项将位图保存为。就完成了!

但是我们有点超前了。在导出之前,您应该检查一些重要的设置。

导出和字体选项设置
  • 字体设置:这使您可以选择要使用的字体和大小。这里最重要的项目是“匹配字符高度”。默认情况下,应用程序的“大小”选项使用像素而不是点。您会看到图形编辑器的字体大小和生成的字体大小之间存在很大差异。如果您希望字形有意义,请选择“匹配字符高度”选项。
  • 导出设置:对于导出,请确保纹理大小是二的幂(例如 16×16、32×32、64×64 等)。然后您就可以在需要时利用“线性 Mipmap 线性”过滤。

在设置的底部,您会看到“文件格式”部分。选择这里的任何选项都可以,只要您可以读取文件并创建字形即可。

如果您正在寻找最小的文件大小,我进行了一项非常不科学的测试,我创建了一个包含所有小写和大写拉丁字符的位图,并比较了每个选项。对于字体描述符,最有效的格式是二进制

字体描述符格式 文件大小
二进制 3 KB
原始文本 11 KB
XML 12 KB
纹理格式 文件大小
PNG 7 KB
Targa 64 KB
DirectDraw 表面 65 KB

PNG 是文本纹理最小的文件大小。

当然,这比仅仅考虑文件大小要复杂一些。为了更好地了解应该使用哪种选项,你应该研究解析时间和运行时性能。如果你想知道每种格式的优缺点,请查看此讨论

如何使用位图字体

创建位图字体几何体比仅仅使用纹理要复杂一些,因为我们必须自己构建字符串。每个字形都有自己的高度和宽度,并且对纹理的不同部分进行采样。我们必须为字符串上的每个字形创建一个四边形,以便我们可以为它们提供正确的 UV 来对它的字形进行采样。

你可以在 ThreeJS 中使用 three-bmfont-text 使用位图、SDF 和 MSDF 创建字符串。它可以处理多行文本,并将所有字形批处理到单个几何体上。注意,它需要从 npm 在项目中进行安装。

var createGeometry = require('three-bmfont-text')
var loadFont = require('load-bmfont')

loadFont('fonts/Arial.fnt', function(err, font) {
  // create a geometry of packed bitmap glyphs, 
  // word wrapped to 300px and right-aligned
  var geometry = createGeometry({
    font: font,
    text: "My Text"
  })
    
  var textureLoader = new THREE.TextureLoader();
  textureLoader.load('fonts/Arial.png', function (texture) {
    // we can use a simple ThreeJS material
    var material = new THREE.MeshBasicMaterial({
      map: texture,
      transparent: true,
      color: 0xaaffff
    })

    // now do something with our mesh!
    var mesh = new THREE.Mesh(geometry, material)
  })
})

根据你的文本是绘制为全黑还是全白,使用反转选项。

优点

  • 它快速且简单地渲染。
  • 它是一个 1:1 的比例,并且与分辨率无关。
  • 它可以渲染任何字符串,只要有字形。
  • 它适用于大量需要经常更改的文本。
  • 它在字形数量有限的情况下非常有效。
  • 它包括对运行时字距调整、行高和自动换行等功能的支持。

缺点

  • 它只接受有限的字符集和样式。
  • 它需要预先光栅化字形,并且需要额外的 bin 打包以实现最佳使用。
  • 它在较大比例时会变得模糊和像素化,并且也可以旋转或变换。
  • 每个渲染的字形只有一个四边形。

交互式演示:闪灵片尾字幕

光栅位图字体非常适合电影片尾字幕,因为我们只需要几种大小和样式。缺点是文本在响应式设计中效果不佳,因为在较大尺寸下它会看起来模糊和像素化。

对于鼠标效果,我通过将鼠标位置映射到视图的大小来进行计算,然后计算鼠标到文本位置的距离。我也在文本在 z 轴和 y 轴上的特定点命中时旋转文本。

符号距离场

与位图字体非常类似,符号距离场 (SDF) 字体也是一种 纹理图集。独特的字形被批处理到单个“纹理图集”中,该图集可以在运行时创建任何字符串。

但与位图字体不同,SDF 字体不是将光栅化后的字形存储在纹理中,而是生成并存储字形的 SDF,这使得可以使用低分辨率图像生成高分辨率形状。

像多边形网格(字体几何体)一样,SDF表示形状SDF 上的每个像素存储到最近表面的距离。符号指示像素是在形状内部还是外部。如果符号为负,则像素在内部;如果为正,则像素在外部。此视频很好地说明了这个概念。

SDF 也常用于 光线追踪体积渲染

由于 SDF 在每个像素中存储距离,因此原始结果看起来像是原始形状的模糊版本。为了输出硬形状,你需要在 0.5 处对其进行 alpha 测试,这是字形的边界。看一下字母“A”的 SDF 与其常规光栅图像的比较

光栅文本位于原始和 alpha 测试后的 SDF 旁边

正如我之前提到的,SDF 的最大优势是可以从低分辨率 SDF 渲染高分辨率形状。这意味着你可以创建一个 16pt 字体的 SDF,并将文本放大到 100pt 或更高,而不会损失太多清晰度。

SDF 在缩放方面表现出色,因为你可以使用双线性插值几乎完美地重建距离,这是一种表示我们可以获得两个点之间值的复杂说法。在本例中,对常规位图字体上的两个像素进行双线性插值会得到中间颜色,从而导致线性模糊。

SDF 上,对两个像素进行双线性插值会得到到最近边的中间距离。由于这两个像素距离一开始就非常相似,因此结果值不会丢失关于字形的大量信息。这也意味着 SDF 越大,丢失的信息越少,精度越高。

然而,这个过程有一个问题。如果像素之间的变化率不是线性的(比如尖锐的角),则双线性插值会得到一个不准确的值,导致将 SDF 缩放至远大于其原始尺寸时出现缺口或圆角。

SDF 圆角

除了扩大纹理尺寸外,唯一真正的解决方案是使用多通道 SDF,这是我们将在下一节中介绍的内容。

如果你想更深入地了解 SDF 背后的科学原理,请查看 Chris Green 的硕士论文(PDF)来了解这个主题。

优点

  • 它们即使在旋转、缩放或变换时也能保持清晰度。
  • 它们非常适合动态文本。
  • 它们提供良好的质量与尺寸比率。单个纹理可用于渲染微小和巨大的字体尺寸,而不会损失太多质量。
  • 它们每个字形只有四个顶点,顶点数量很少。
  • 抗锯齿成本低廉,使用 alpha 测试可以轻松制作边框、阴影和 各种效果
  • 它们比 MSDF(我们将在稍后看到)更小。

缺点

  • 当纹理采样超过其分辨率时,它们会导致圆角或缺口。 (同样,我们将看到 MSDF 如何防止这种情况。)
  • 它们在微小的字体尺寸下无效。
  • 它们只能与单色字形一起使用。

多通道符号距离场

多通道符号距离场 (MSDF) 字体是一个比较拗口的词,它是一种相对较新的 SDF 变体,可以通过使用所有三个颜色通道来生成几乎完美的锐利角。它们乍看起来非常令人震惊,但不要被吓倒,因为它们比看起来更容易使用。

多通道符号距离场文件乍看起来可能有点吓人。

使用所有三个颜色通道确实会导致图像更重,但这使得 MSDF 具有远优于常规 SDF 的质量与空间比率。下图显示了 SDFMSDF 之间的差异,它们都已缩放至 50px。

SDF 字体会导致圆角,即使在 1 倍缩放时也是如此,而 MSDF 字体则保留了锐利的边缘,即使在 5 倍缩放时也是如此。

与常规 SDF 一样,MSDF 存储到最近边的距离,但在找到锐利角时会更改颜色通道。我们通过绘制 两个或多个颜色通道一致的地方 来获得形状。尽管 涉及更多技术。请查看 此 MSDF 生成器的自述文件 以获取更详细的解释。

优点

  • 它们比 SDF 支持更高的质量和空间比率,通常是更好的选择。
  • 它们在缩放时保持锐利的角。

缺点

  • 它们可能包含 小型伪像,但可以通过扩大字形的纹理大小来避免这些伪像。
  • 它们需要在运行时过滤三个值的中间值,这有点昂贵。
  • 它们仅与单色字形兼容。

如何创建 MSDF 字体

创建 MSDF 字体的最快方法是使用 msdf-bmfont-web 工具。它具有大多数相关的自定义选项,并且可以在几秒钟内完成工作,全部在浏览器中完成。或者,A-Frame 的团队已经将一些 Google 字体转换为 MSDF

如果你还想生成 SDF 或你的字体,由于某些有问题的字形,它需要进行一些特殊调整。msdf-bmfont-xml CLI 为你提供了广泛的选项,而不会让事情过于混乱。让我们看一下如何使用它。

首先,你需要从 npm 全局安装它

npm install msdf-bmfont-xml -g

接下来,用你的选项提供一个 .ttf 字体文件

msdf-bmfont ./Open-Sans-Black.ttf --output-type json --font-size 76 --texture-size 512,512

这些选项值得深入研究一下。虽然 msdf-bmfont-xml 提供了许多选项来微调你的字体,但实际上你可能只需要几个选项来正确生成 MSDF

  • -t <type><code>--field-type <msdf or sdf>:`msdf-bmfont-xml` 默认情况下会生成 MSDF 字形图集。如果您想生成 SDF,则需要使用 `-t sdf` 指定。
  • -f <xml or json>--output-type <xml or json>:`msdf-bmfont-xml` 会生成一个 XML 字体文件,您需要在运行时将其解析为 JSON。您可以通过直接导出 JSON 来避免此解析步骤。
  • -s, --font-size <fontSize>:如果字体大小非常小,可能会出现一些伪影和缺陷。大多数情况下,增大字体大小可以消除它们。此示例 显示了字母“M”中的一个小缺陷。
  • -m <w,h>--texture-size <w,h>:如果所有字符都无法容纳在同一纹理中,则会创建一个第二个纹理来容纳它们。除非您试图利用多页字形图集,否则我建议您增加纹理大小,使其能够容纳所有字符在一个纹理上,以避免额外的操作。

还有其他工具可以帮助生成 MSDFSDF 字体

  • msdf-bmfont-web:一个快速轻松地创建 MSDF(但不是 SDF)的 Web 工具
  • msdf-bmfont:一个使用 Cairo 和 node-canvas 的 Node 工具
  • msdfgen:所有其他 MSDF 工具的基础,最初的命令行工具
  • Hiero:一个用于生成位图和 SDF 字体的工具

如何使用 SDF 和 MSDF 字体

因为 SDFMSDF 字体也是字形图集,所以我们可以像使用位图字体一样使用 three-bmfont-text。唯一的区别是,我们必须使用片段着色器从距离场中获取字形。

以下是 SDF 字体的实现方式。由于我们的距离场在字形外部的值大于 0.5,在字形内部的值小于 0.5,因此我们需要在片段着色器中对每个像素进行 alpha 测试,以确保我们只渲染距离小于 0.5 的像素,从而只渲染字形的内部。

const fragmentShader = `

  uniform vec3 color;
  uniform sampler2D map;
  varying vec2 vUv;
  
  void main(){
    vec4 texColor = texture2D(map, vUv);
    // Only render the inside of the glyph.
    float alpha = step(0.5, texColor.a);

    gl_FragColor = vec4(color, alpha);
    if (gl_FragColor.a < 0.0001) discard;
  }
`;

const vertexShader = `
  varying vec2 vUv;   
  void main {
    gl_Position = projectionMatrix * modelViewMatrix * position;
    vUv = uv;
  }
`;

let material = new THREE.ShaderMaterial({
  fragmentShader, vertexShader,
  uniforms: {
    map: new THREE.Uniform(glyphs),
    color: new THREE.Uniform(new THREE.Color(0xff0000))
  }
})

类似地,我们可以从 three-bmfont-text 导入字体,它自带抗锯齿功能。然后,我们可以直接在 `RawShaderMaterial` 上使用它。

let SDFShader = require('three-bmfont-text/shaders/sdf');
let material = new THREE.RawShaderMaterial(MSDFShader({
  map: texture,
  transparent: true,
  color: 0x000000
}));

MSDF 字体略有不同。它们通过两个颜色通道的交点来重现锐利的角。两个或多个颜色通道必须对此达成一致。在进行任何 alpha 测试之前,我们需要获取三个颜色通道的中值,以查看它们是否一致。

const fragmentShader = `

  uniform vec3 color;
  uniform sampler2D map;
  varying vec2 vUv;

  float median(float r, float g, float b) {
    return max(min(r, g), min(max(r, g), b));
  }
  
  void main(){
    vec4 texColor = texture2D(map, vUv);
    // Only render the inside of the glyph.
    float sigDist = median(texColor.r, texColor.g, texColor.b) - 0.5;
    float alpha = step(0.5, sigDist);
    gl_FragColor = vec4(color, alpha);
    if (gl_FragColor.a < 0.0001) discard;
  }
`;
const vertexShader = `
  varying vec2 vUv;   
  void main {
    gl_Position = projectionMatrix * modelViewMatrix * position;
    vUv = uv;
  }
`;

let material = new THREE.ShaderMaterial({
  fragmentShader, vertexShader,
  uniforms: {
    map: new THREE.Uniform(glyphs),
    color: new THREE.Uniform(new THREE.Color(0xff0000))
  }
})

同样,我们也可以从 three-bmfont-text 导入,使用其 `MSDFShader`,它也自带抗锯齿功能。然后,我们可以直接在 `RawShaderMaterial` 上使用它。

let MSDFShader = require('three-bmfont-text/shaders/msdf');
let material = new THREE.RawShaderMaterial(MSDFShader({
  map: texture,
  transparent: true,
  color: 0x000000
}));

演示:星球大战开场动画

星球大战拖长音的开场动画是一个很好的例子,其中 MSDFSDF 字体非常有效,因为该效果需要文本以多种大小出现。我们可以使用单个 MSDF,文本始终看起来很清晰!尽管遗憾的是,three-bm-font 还不支持对齐文本。应用左对齐将使演示更加平衡。

对于光剑效果,我正在对一个与平面大小相同的不可见平面进行射线投射,绘制到一个大小相同的画布上,并通过将场景位置映射到纹理坐标来采样该画布。

额外提示:使用高度贴图生成 3D 文本

除了字体几何体之外,我们介绍的所有技术都在单个四边形上生成字符串或字形。如果您想用平面纹理构建 3D 几何体,最好的选择是使用高度贴图。

高度贴图是一种使用纹理来提升几何体高度的技术。这通常用于 在游戏中生成山脉,但事实证明,它对 渲染文本 也很有用。

唯一需要注意的是,为了使文本看起来光滑,您需要很多面。

进一步阅读

不同的情况需要不同的技术。我们这里看到的都不是万能药,它们都有各自的优点和缺点。

有很多工具和库可以帮助您最大程度地利用 WebGL 文本,其中大多数实际上源自 WebGL 之外。如果您想继续学习,我强烈建议您超越 WebGL 并查看以下一些链接。