From fc5e1f9a6856b42954e7a53311c6cbd1cf71b40d Mon Sep 17 00:00:00 2001 From: Meow J Date: Thu, 6 Oct 2016 02:36:12 +0800 Subject: [PATCH] 06-02 --- docs/06 In Practice/02 Text Rendering.md | 154 +++++++++++------------ glossary.md | 10 ++ mkdocs.yml | 2 +- 3 files changed, 88 insertions(+), 78 deletions(-) diff --git a/docs/06 In Practice/02 Text Rendering.md b/docs/06 In Practice/02 Text Rendering.md index 3cb52fd..c407455 100644 --- a/docs/06 In Practice/02 Text Rendering.md +++ b/docs/06 In Practice/02 Text Rendering.md @@ -1,4 +1,4 @@ -# 文字渲染 +# 文本渲染 原文 | [Text Rendering](http://learnopengl.com/#!In-Practice/Text-Rendering) ---|--- @@ -6,31 +6,31 @@ 翻译 | [Geequlim](http://geequlim.com) 校对 | gjy_1992, [BLumia](https://github.com/blumia/) -当你在图形计算领域冒险到了一定阶段以后你可能会想使用OpenGL来绘制文字。然而,可能与你想象的并不一样,使用像OpenGL这样的底层库来把文字渲染到屏幕上并不是一件简单的事情。如果你你只需要绘制128种字符,那么事情可能会简单一些。但是当我们要绘制的字符有着不同的宽、高和边距;如果你使用的语言中不止包含128个字符;当你要绘制音乐符、数学符号;以及考虑把如何处理文本自动转行等等情况考虑进来的时候...事情马上就会变得复杂得多,你甚至觉得这些工作并不属于像OpenGL这样的底层图形库该讨论的范畴。 +当你在图形计算领域冒险到了一定阶段以后你可能会想使用OpenGL来绘制文本。然而,可能与你想象的并不一样,使用像OpenGL这样的底层库来把文本渲染到屏幕上并不是一件简单的事情。如果你只需要绘制128种不同的字符(Character),那么事情可能会简单一些。但是如果你要绘制的字符有着不同的宽、高和边距,事情马上就复杂了。根据你使用语言的不同,你可能会需要多于128个字符。再者,如果你要绘制音乐符、数学符号这些特殊的符号;或者渲染竖排文本呢?一旦你把文本这些复杂的情况考虑进来,你就不会奇怪为什么OpenGL这样的底层API没有包含文本处理了。 -由于OpenGL本身并没有定义如何渲染文字到屏幕,也没有用于表示文字的基本图形,我们必须自己定义一套全新的方式才能让OpenGL来绘制文字。目前一些技术包括:通过**`GL_LINES`**来绘制字形、创建文字的3D网格、将带有文字的纹理渲染到一个2D方块中。 +由于OpenGL本身并没有包含任何的文本处理能力,我们必须自己定义一套全新的系统让OpenGL绘制文本到屏幕上。由于文本字符没有图元,我们必须要有点创造力才行。需要使用的一些技术可以是:通过GL_LINES来绘制字形,创建文本的3D网格(Mesh),或在3D环境中将字符纹理渲染到2D四边形上。 -开发者最常用的一种方式是将字符纹理绘制到矩形方块上。绘制这些纹理方块其实并不是很复杂,然而检索要绘制的文字的纹理却变成了一项挑战性的工作。本教程将探索多种文字渲染的实现方法,并且着重对更加现代而且灵活的渲染技术(使用FreeType库)进行讲解。 +开发者最常用的一种方式是将字符纹理绘制到四边形上。绘制这些纹理四边形本身其实并不是很复杂,然而检索要绘制文本的纹理却变成了一项有挑战性的工作。本教程将探索多种文本渲染的实现方法,并且使用FreeType库实现一个更加高级但更灵活的渲染文本技术。 -## 经典文字渲染:位图字体 +## 经典文本渲染:位图字体 -在早期渲染文字时,选择你应用程序的字体(或者创建你自己的字体)来绘制文字是通过将所有用到的文字加载在一张大纹理图中来实现的。这张纹理贴图我们把它叫做**位图字体(Bitmap Font)**,它包含了所有我们想要使用的字符。这些字符被称为**字形(Glyph)**。每个字形根据他们的编号被放到位图字体中的确切位置,在渲染这些字形的时候根据这些排列规则将他们取出并贴到指定的2D方块中。 +早期的时候,渲染文本是通过选择一个需要的字体(Font)(或者自己创建一个),并提取这个字体中所有相关的字符,将它们放到一个单独的大纹理中来实现的。这样一张纹理叫做位图字体(Bitmap Font),它在纹理的预定义区域中包含了我们想要使用的所有字符。字体的这些字符被称为字形(Glyph)。每个字形都关联着一个特定的纹理坐标区域。当你想要渲染一个字符的时候,你只需要通过渲染这一块特定的位图字体区域到2D四边形上即可。 ![](../img/06/02/bitmapfont.png) -上图展示了我们如何从一张位图字体的纹理中通过对字形的合理取样(通过小心地选择字形的纹理坐标)来实现绘制文字“OpenGL”到2D方块中的原理。通过对OpenGL启用混合并让位图字体的纹理背景保持透明,这样就能实现使用OpenGL绘制你想要文字到屏幕的功能。上图的这张位图字体是使用[Codehead的位图字体生成器](http://www.codehead.co.uk/cbfg/)生成的。 +你可以看到,我们取一张位图字体,(通过仔细选择纹理坐标)从纹理中采样对应的字形,并渲染它们到多个2D四边形上,最终渲染出“OpenGL”文本。通过启用[混合](../04 Advanced OpenGL/03 Blending.md),让背景保持透明,最终就能渲染一个字符串到屏幕上。这个位图字体是通过Codehead的位图[字体生成器](http://www.codehead.co.uk/cbfg/)生成的。 -使用这种途径来绘制文字有许多优势也有很多缺点。首先,它相对来说很容易实现,并且因为位图字体被预渲染好,使得这种方法效率很高。然而,这种途径并不够灵活。当你想要使用不同的字体时,你不得不重新生成位图字体,以及你的程序会被限制在一个固定的分辨率:如果你对这些文字进行放大的话你会看到文字的像素边缘。此外,这种方式仅局限于用来绘制很少的字符,如果你想让它来扩展支持Unicode文字的话就很不现实了。 +使用这种方式绘制文本有许多优势也有很多缺点。首先,它相对来说很容易实现,并且因为位图字体已经预光栅化了,它的效率也很高。然而,这种方式不够灵活。当你想要使用不同的字体时,你需要重新编译一套全新的位图字体,而且你的程序会被限制在一个固定的分辨率。如果你对这些文本进行缩放的话你会看到文本的像素边缘。此外,这种方式通常会局限于非常小的字符集,如果你想让它来支持Extended或者Unicode字符的话就很不现实了。 -这种绘制文字的方式曾经得益于它的高速和可移植性而非常流行,然而现在已经存在更加灵活的方式了。其中一个是我们即将展开讨论的使用FreeType库来加载TrueType字体的方式。 +这种绘制文本的方式曾经得益于它的高速和可移植性而非常流行,然而现在已经出现更加灵活的方式了。其中一个是我们即将讨论的使用FreeType库来加载TrueType字体的方式。 -## 现代文字渲染:FreeType +## 现代文本渲染:FreeType -FreeType是一个能够用于加载字体并将他们渲染到位图以及提供多种字体相关的操作的软件开发库。它是一个非常受欢迎的跨平台字体库,被用于 Mac OSX、Java、PlayStation主机、Linux、Android等。FreeType的真正吸引力在于它能够加载TrueType字体。 +FreeType是一个能够用于加载字体并将他们渲染到位图以及提供多种字体相关的操作的软件开发库。它是一个非常受欢迎的跨平台字体库,它被用于Mac OS X、Java、PlayStation主机、Linux、Android等平台。FreeType的真正吸引力在于它能够加载TrueType字体。 -TrueType字体不采用像素或其他不可缩放的方式来定义,而是一些通过数学公式(曲线的组合)。这些字形,类似于矢量图像,可以根据你需要的字体大小来生成像素图像。通过使用TrueType字体可以轻易呈现不同大小的字符符号并且没有任何质量损失。 +TrueType字体不是用像素或其他不可缩放的方式来定义的,它是通过数学公式(曲线的组合)来定义的。类似于矢量图像,这些光栅化后的字体图像可以根据需要的字体高度来生成。通过使用TrueType字体,你可以轻易渲染不同大小的字形而不造成任何质量损失。 -FreeType可以在他们的[官方网站](http://www.freetype.org/)中下载到。你可以选择自己编译他们提供的源代码或者使用他们已编译好的针对你的平台的链接库。请确认你将freetype.lib添加到你项目的链接库中,同时你还要添加它的头文件目录到项目的搜索目录中。 +FreeType可以在他们的[官方网站](http://www.freetype.org/)中下载到。你可以选择自己用源码编译这个库,如果支持你的平台的话,你也可以使用他们预编译好的库。请确认你将**freetype.lib**添加到你项目的链接库中,并且确认编译器知道头文件的位置。 然后请确认包含合适的头文件: @@ -39,13 +39,13 @@ FreeType可以在他们的[官方网站](http://www.freetype.org/)中下载到 #include FT_FREETYPE_H ``` -!!! Attention +!!! attention - 由于FreeType开发得比较早(至少在我写这篇文章以前就已经开发好了),你不能将它们的头文件放到一个新的目录下,它们应该保存在你包含目录的根目录下。通过使用像 `#include ` 这样的方式导入FreeType可能会出现各种头文件冲突的问题。 + 由于FreeType的开发方式(至少在我写这篇文章的时候),你不能将它们的头文件放到一个新的目录下。它们应该保存在你include目录的根目录下。通过使用像 `#include ` 这样的方式导入FreeType可能会出现一些头文件冲突的问题。 -FreeType要做的事就是加载TrueType字体并为每一个字形生成位图和几个度量值。我们可以取出它生成的位图作为字形的纹理,将这些度量值用作字形纹理的位置、偏移等描述。 +FreeType所做的事就是加载TrueType字体并为每一个字形生成位图以及计算几个度量值(Metric)。我们可以提取出它生成的位图作为字形的纹理,并使用这些度量值定位字符的字形。 -要加载一个字体,我们需要做的是初始化FreeType并且将这个字体加载为FreeType称之为面(Face)的东西。这里为我们加载一个从Windows/Fonts目录中拷贝来的TrueType字体文件arial.ttf。 +要加载一个字体,我们只需要初始化FreeType库,并且将这个字体加载为一个FreeType称之为(Face)的东西。这里为我们加载一个从**Windows/Fonts**目录中拷贝来的TrueType字体文件**arial.ttf**。 ```c++ FT_Library ft; @@ -54,60 +54,58 @@ if (FT_Init_FreeType(&ft)) FT_Face face; if (FT_New_Face(ft, "fonts/arial.ttf", 0, &face)) - std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl; + std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl; ``` -这些FreeType函数在出现错误的情况下返回一个非零整数值。 +这些FreeType函数在出现错误时将返回一个非零的整数值。 -一旦我们加载字体面完成,我们还要定义文字大小,这表示着我们要从字体面中生成多大的字形: +当面加载完成之后,我们需要定义字体大小,这表示着我们要从字体面中生成多大的字形: ```c++ FT_Set_Pixel_Sizes(face, 0, 48); ``` -此函数设置了字体面的宽度和高度,将宽度值设为0表示我们要从字体面通过给出的高度中动态计算出字形的宽度。 +此函数设置了字体面的宽度和高度,将宽度值设为0表示我们要从字体面通过给定的高度中动态计算出字形的宽度。 -一个字体面中包含了所有字形的集合。我们可以通过调用`FT_Load_Char`函数来激活当前要表示的字形。这里我们选在加载字母字形'X': +一个FreeType面中包含了一个字形的集合。我们可以调用FT_Load_Char函数来将其中一个字形设置为激活字形。这里我们选择加载字符字形'X': ```c++ if (FT_Load_Char(face, 'X', FT_LOAD_RENDER)) - std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl; + std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl; ``` -通过将**`FT_LOAD_RENDER`**设为一个加载标识,我们告诉FreeType去创建一个8位的灰度位图,我们可以通过`face->glyph->bitmap`来取得这个位图。 +通过将FT_LOAD_RENDER设为加载标记之一,我们告诉FreeType去创建一个8位的灰度位图,我们可以通过`face->glyph->bitmap`来访问这个位图。 -使用FreeType加载的字形位图并不像我们使用位图字体那样持有相同的尺寸大小。使用FreeType生产的字形位图的大小是恰好能包含这个字形的尺寸。例如生产用于表示'.'的位图的尺寸要比表示'X'的小得多。因此,FreeType在加载字形的时候还生产了几个度量值来描述生成的字形位图的大小和位置。下图展示了FreeType的所有度量值的涵义。 +使用FreeType加载的每个字形没有相同的大小(不像位图字体那样)。使用FreeType生成的位图的大小恰好能包含这个字符可见区域。例如生成用于表示'.'的位图的大小要比表示'X'的小得多。因此,FreeType同样也加载了一些度量值来指定每个字符的大小和位置。下面这张图展示了FreeType对每一个字符字形计算的所有度量值。 ![](../img/06/02/glyph.png) -每一个字形都放在一个水平的基线(Baseline)上,上图中被描黑的水平箭头表示该字形的基线。这条基线类似于拼音四格线中的第二根水平线,一些字形被放在基线以上(如'x'或'a'),而另一些则会超过基线以下(如'g'或'p')。FreeType的这些度量值中包含了字形在相对于基线上的偏移量用来描述字形相对于此基线的位置,字形的大小,以及与下一个字符之间的距离。下面列出了我们在渲染字形时所需要的度量值的属性: - - +每一个字形都放在一个水平的基准线(Baseline)上(即上图中水平箭头指示的那条线)。一些字形恰好位于基准线上(如'X'),而另一些则会稍微越过基准线以下(如'g'或'p')(译注:即这些带有下伸部的字母,可以见[这里](https://www.supremo.tv/typeterms/))。这些度量值精确定义了摆放字形所需的每个字形距离基准线的偏移量,每个字形的大小,以及需要预留多少空间来渲染下一个字形。下面这个表列出了我们需要的所有属性。 属性|获取方式|生成位图描述 ----|--- -width | face->glyph->bitmap.width | 宽度,单位:像素 -height | face->glyph->bitmap.rows | 高度,单位:像素 -bearingX| face->glyph->bitmap_left| 水平位置(相对于起点origin),单位:像素 -bearingY| face->glyph->bitmap_top | 垂直位置(相对于基线Baseline),单位:像素 -advance | face->glyph->advance.x | 起点到下一个字形的起点间的距离(单位:1/64像素) +---|---|--- +**width** | `face->glyph->bitmap.width` | 位图宽度(像素) +**height** | `face->glyph->bitmap.rows` | 位图高度(像素) +**bearingX** | `face->glyph->bitmap_left` | 水平距离,即位图相对于原点的水平位置(像素) +**bearingY** | `face->glyph->bitmap_top` | 垂直距离,即位图相对于基准线的垂直位置(像素) +**advance** | `face->glyph->advance.x` | 水平预留值,即原点到下一个字形原点的水平距离(单位:1/64像素) -在我们想要渲染字符到屏幕的时候就能根据这些度量值来生成对应字形的纹理了,然而每次渲染都这样做显然不是高效的。我们应该将这些生成的数据储存在应用程序中,在渲染过程中再去取。方便起见,我们需要定义一个用来储存这些属性的结构体,并创建一个字符表来存储这些字形属性。 +在需要渲染字符时,我们可以加载一个字符字形,获取它的度量值,并生成一个纹理,但每一帧都这样做会非常没有效率。我们应将这些生成的数据储存在程序的某一个地方,在需要渲染字符的时候再去调用。我们会定义一个非常方便的结构体,并将这些结构体存储在一个map中。 ```c++ struct Character { - GLuint TextureID; // 字形纹理ID - glm::ivec2 Size; // 字形大大小 - glm::ivec2 Bearing; // 字形基于基线和起点的位置 - GLuint Advance; // 起点到下一个字形起点的距离 + GLuint TextureID; // 字形纹理的ID + glm::ivec2 Size; // 字形大小 + glm::ivec2 Bearing; // 从基准线到字形左部/顶部的偏移值 + GLuint Advance; // 原点距下一个字形原点的距离 }; std::map Characters; ``` -本教程本着让一切简单的目的,我们只生成表示128个ASCII字符的字符表。并为每一个字符储存纹理和一些度量值。这样,所有需要的字符就被存下来备用了。 +对于这个教程来说,本着让一切简单的目的,我们只生成ASCII字符集的前128个字符。对每一个字符,我们生成一个纹理并保存相关数据至Character结构体中,之后再添加至Characters这个映射表中。这样子,渲染一个字符所需的所有数据就都被储存下来备用了。 ```c++ -glPixelStorei(GL_UNPACK_ALIGNMENT, 1); //禁用byte-alignment限制 +glPixelStorei(GL_UNPACK_ALIGNMENT, 1); //禁用字节对齐限制 for (GLubyte c = 0; c < 128; c++) { // 加载字符的字形 @@ -116,7 +114,7 @@ for (GLubyte c = 0; c < 128; c++) std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl; continue; } - // 生成字形纹理 + // 生成纹理 GLuint texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); @@ -136,7 +134,7 @@ for (GLubyte c = 0; c < 128; c++) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // 将字符存储到字符表中备用 + // 储存字符供之后使用 Character character = { texture, glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), @@ -147,15 +145,15 @@ for (GLubyte c = 0; c < 128; c++) } ``` -在这个for循环中我们将所有ASCII中的128个字符设置合适的字形。为每一个字符创建纹理并存储它们的部分度量值。有趣的是我们这里将纹理的格式设置为**GL_RED**。通过字形生成的位图是8位灰度图,他的每一个颜色表示为一个字节。因此我们需要将每一位都作为纹理的颜色值。我们通过创建每一个字节表示一个颜色的红色分量(颜色分量的第一个字节)来创建字形纹理。我们想用一个字节来表示纹理颜色,这需要提前通知OpenGL +在这个for循环中我们遍历了ASCII集中的全部128个字符,并获取它们对应的字符字形。对每一个字符,我们生成了一个纹理,设置了它的选项,并储存了它的度量值。有趣的是我们这里将纹理的`internalFormat`和`format`设置为GL_RED。通过字形生成的位图是一个8位灰度图,它的每一个颜色都由一个字节来表示。因此我们需要将位图缓冲的每一字节都作为纹理的颜色值。这是通过创建一个特殊的纹理实现的,这个纹理的每一字节都对应着纹理颜色的红色分量(颜色向量的第一个字节)。如果我们使用一个字节来表示纹理的颜色,我们需要注意OpenGL的一个限制: ```c++ glPixelStorei(GL_UNPACK_ALIGNMENT, 1); ``` -OpenGL要求所有的纹理颜都必须是4字节队列,这样纹理的内存大小就一定是4字节的整数倍。通常这并不会出现什么问题,因为通常的纹理都有4或者4的整数被的储存大小,但是现在我们只用一位来表示每一个像素颜色,此时的纹理可能有任意内存长度。通过将纹理解压参数设为1,这样才能确保不会有内存对齐的解析问题。 +OpenGL要求所有的纹理都是4字节对齐的,即纹理的大小永远是4字节的倍数。通常这并不会出现什么问题,因为大部分纹理的宽度都为4的倍数并/或每像素使用4个字节,但是现在我们每个像素只用了一个字节,它可以是任意的宽度。通过将纹理解压对齐参数设为1,这样才能确保不会有对齐问题(它可能会造成段错误)。 -当你处理完字形后同样不要忘记清理FreeType的资源。 +当你处理完字形后不要忘记清理FreeType的资源。 ```c++ FT_Done_Face(face); @@ -164,7 +162,7 @@ FT_Done_FreeType(ft); ### 着色器 -我们需要使用下面的顶点着色器来渲染字形: +我们将使用下面的顶点着色器来渲染字形: ```c++ #version 330 core @@ -177,10 +175,10 @@ void main() { gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); TexCoords = vertex.zw; -} +} ``` -我们将位置和纹理纹理坐标的数据合起来存在一个`vec4`中。顶点着色器将会将位置坐标与投影矩阵相乘,并将纹理坐标转发给片段着色器: +我们将位置和纹理纹理坐标的数据合起来存在一个vec4中。这个顶点着色器将位置坐标与一个投影矩阵相乘,并将纹理坐标传递给片段着色器: ```c++ #version 330 core @@ -194,26 +192,27 @@ void main() { vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); color = vec4(textColor, 1.0) * sampled; -} +} ``` -片段着色器有两个uniform变量:一个是单颜色通道的字形位图纹理,另一个是文字的颜色,我们可以同调整它来改变最终输出的字体颜色。我们首先从位图纹理中采样,由于纹理数据中仅存储着红色分量,我们就通过**`r`**分量来作为取样颜色的aplpha值。结果值是一个字形背景为纯透明,而字符部分为不透明的白色的颜色。我们将此颜色与字体颜色uniform值相乘就得到了要输出的字符颜色了。 +片段着色器有两个uniform变量:一个是单颜色通道的字形位图纹理,另一个是颜色uniform,它可以用来调整文本的最终颜色。我们首先从位图纹理中采样颜色值,由于纹理数据中仅存储着红色分量,我们就采样纹理的**r**分量来作为取样的alpha值。通过变换颜色的alpha值,最终的颜色在字形背景颜色上会是透明的,而在真正的字符像素上是不透明的。我们也将RGB颜色与textColor这个uniform相乘,来变换文本颜色。 -当然我们必需开启混合才能让这一切行之有效: +当然我们需要启用[混合](../04 Advanced OpenGL/03 Blending.md)才能让这一切行之有效: ```c++ glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); ``` -至于投影矩阵我们将使用一个正交投影矩阵。对于文字渲染我们通常都不需要进行透视,使用正交投影也能保证我们所有的顶点坐标设置有效: + +对于投影矩阵,我们将使用一个正射投影矩阵(Orthographic Projection Matrix)。对于文本渲染我们(通常)都不需要透视,使用正射投影同样允许我们在屏幕坐标系中设定所有的顶点坐标,如果我们使用如下方式配置: ```c++ glm::mat4 projection = glm::ortho(0.0f, 800.0f, 0.0f, 600.0f); ``` -我们设置投影矩阵的底部参数为0.0f并将顶部参数设置为窗口的高度。这样做的结果是我们可以通过设置0.0f~600.0f的纵坐标来表示顶点在窗口中的垂直位置。这意味着现在点(0.0,0.0)表示左下角而不再是窗口正中间。 +我们设置投影矩阵的底部参数为`0.0f`,并将顶部参数设置为窗口的高度。这样做的结果是我们指定了y坐标的范围为屏幕底部(0.0f)至屏幕顶部(600.0f)。这意味着现在点(0.0, 0.0)对应左下角(译注:而不再是窗口正中间)。 -最后要做的事是创建一个VBO和VAO用来渲染方块。现在我们分配足够的内存来初始化VBO然后在我们渲染字符的时候再来更新VBO的内存。 +最后要做的事是创建一个VBO和VAO用来渲染四边形。现在我们在初始化VBO时分配足够的内存,这样我们可以在渲染字符的时候再来更新VBO的内存。 ```c++ GLuint VAO, VBO; @@ -228,24 +227,24 @@ glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); ``` -每个2D方块需要6个顶点,每个顶点又是由一个4维向量(一个纹理坐标和一个顶点坐标)组成,因此我们将VBO的内存分配为6*4个float的大小。由于我们会在绘制字符时更新这断内存,所以我们将内存类型设置为`GL_DYNAMIC_DRAW` +每个2D四边形需要6个顶点,每个顶点又是由一个4float向量(译注:一个纹理坐标和一个顶点坐标)组成,因此我们将VBO的内存分配为6 * 4个float的大小。由于我们会在绘制字符时经常更新VBO的内存,所以我们将内存类型设置为GL_DYNAMIC_DRAW。 -### 渲染一行文字 +### 渲染一行文本 -要渲染一个字符,我们从之前创建的字符表中取出一个字符结构体,根据字符的度量值来计算方块的大小。根据方块的大小我们就能计算出6个描述方块的顶点,并使用`glBufferSubData`函数将其更新到VBO所在内内存中。 +要渲染一个字符,我们从之前创建的Characters映射表中取出对应的Character结构体,并根据字符的度量值来计算四边形的维度。根据四边形的维度我们就能动态计算出6个描述四边形的顶点,并使用glBufferSubData函数更新VBO所管理内存的内容。 -我们创建一个函数用来渲染一行文字: +我们创建一个叫做RenderText的函数渲染一个字符串: ```c++ void RenderText(Shader &s, std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color) { - // 激活合适的渲染状态 + // 激活对应的渲染状态 s.Use(); glUniform3f(glGetUniformLocation(s.Program, "textColor"), color.x, color.y, color.z); glActiveTexture(GL_TEXTURE0); glBindVertexArray(VAO); - // 对文本中的所有字符迭代 + // 遍历文本中所有的字符 std::string::const_iterator c; for (c = text.begin(); c != text.end(); c++) { @@ -256,7 +255,7 @@ void RenderText(Shader &s, std::string text, GLfloat x, GLfloat y, GLfloat scale GLfloat w = ch.Size.x * scale; GLfloat h = ch.Size.y * scale; - // 当前字符的VBO + // 对每个字符更新VBO GLfloat vertices[6][4] = { { xpos, ypos + h, 0.0, 0.0 }, { xpos, ypos, 0.0, 1.0 }, @@ -266,23 +265,23 @@ void RenderText(Shader &s, std::string text, GLfloat x, GLfloat y, GLfloat scale { xpos + w, ypos, 1.0, 1.0 }, { xpos + w, ypos + h, 1.0, 0.0 } }; - // 在方块上绘制字形纹理 + // 在四边形上绘制字形纹理 glBindTexture(GL_TEXTURE_2D, ch.textureID); - // 更新当前字符的VBO + // 更新VBO内存的内容 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); glBindBuffer(GL_ARRAY_BUFFER, 0); - // 绘制方块 + // 绘制四边形 glDrawArrays(GL_TRIANGLES, 0, 6); // 更新位置到下一个字形的原点,注意单位是1/64像素 - x += (ch.Advance >> 6) * scale; //(2^6 = 64) + x += (ch.Advance >> 6) * scale; // 位偏移6个单位来获取单位为像素的值 (2^6 = 64) } glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); } ``` -这个函数的内容注释的比较详细了:我们首先计算出方块的起点坐标(即`xpos`和`ypos`)和它的大小(即`w`和`h`),并为该方块生成6个顶点;注意我们在缩放的同时会将部分度量值也进行缩放。接着我们更新方块的VBO、绑定字形纹理来渲染它。 +这个函数的内容应该非常明显了:我们首先计算出四边形的原点坐标(为xposypos)和它的大小(为wh),并生成6个顶点形成这个2D四边形;注意我们将每个度量值都使用scale进行缩放。接下来我们更新了VBO的内容、并渲染了这个四边形。 其中这行代码需要加倍留意: @@ -290,14 +289,13 @@ void RenderText(Shader &s, std::string text, GLfloat x, GLfloat y, GLfloat scale GLfloat ypos = y - (ch.Size.y - ch.Bearing.y); ``` -一些字符(诸如'p'或'q')需要被渲染到基线以下,因此字形方块也应该讲y位置往下调整。调整的量就是便是字形度量值中字形的高度和BearingY的差: +一些字符(如'p'或'q')需要被渲染到基准线以下,因此字形四边形也应该被摆放在RenderTexty值以下。ypos的偏移量可以从字形的度量值中得出: ![](../img/06/02/glyph_offset.png) -要计算这段偏移量的距离,我们需要指出是字形在基线以下的部分最底断到基线的距离。在上图中这段距离用红色双向箭头标出。如你所见,在字形度量值中,我们可以通过用字形的高度减去bearingY来计算这段向量的长度。这段距离有可能是0.0f(如'x'字符) -,或是超出基线底部的距离的长度(如'g'或'j')。 +要计算这段距离,即偏移量,我们需要找出字形在基准线之下延展出去的距离。在上图中这段距离用红色箭头标出。从度量值中可以看到,我们可以通过用字形的高度减去`bearingY`来计算这段向量的长度。对于那些正好位于基准线上的字符(如'X'),这个值正好是0.0。而对于那些超出基准线的字符(如'g'或'j'),这个值则是正的。 -如果你每件事都做对了,那么你现在已经可以使用下面的句子成功地渲染字符串了: +如果你每件事都做对了,那么你现在已经可以使用下面的语句成功渲染字符串了: ```c++ RenderText(shader, "This is sample text", 25.0f, 25.0f, 1.0f, glm::vec3(0.5, 0.8f, 0.2f)); @@ -308,16 +306,18 @@ RenderText(shader, "(C) LearnOpenGL.com", 540.0f, 570.0f, 0.5f, glm::vec3(0.3, 0 ![](../img/06/02/text_rendering.png) -你可以从这里获取这个例子的[源代码](http://learnopengl.com/code_viewer.php?code=in-practice/text_rendering)。 +你可以从[这里](http://learnopengl.com/code_viewer.php?code=in-practice/text_rendering)获取这个例子的源代码。 -通过关闭字形纹理的绑定,能够给你对文字方块的顶点计算更好的理解,它看上去像这样: +为了让你更好理解我们是怎么计算四边形顶点的,我们可以关闭混合来看看真正渲染出来的四边形是什么样子的: ![](../img/06/02/text_rendering_quads.png) -可以看出,对应'p'和'('等字形的方块明显向下偏移了一些,通过这些你就能清楚地看到那条传说中的基线了。 +可以看到,大部分的四边形都位于一条(想象中的)基准线上,而对应'p'或'('字形的四边形则稍微向下偏移了一些。 -## 关于未来 +## 更进一步 -本教程演示了如何使用FreeType绘制TrueType文字。这种方式灵活、可缩放并支持多种字符编码。然而,你的应用程序可能并不需要这么强大的功能,性能更好的点阵字体也许是更可取的。当然你可以结合这两种方法通过动态生成位图字体中所有字符字形。这节省了从大量的纹理渲染器开关和基于每个字形紧密包装可以节省相当的一些性能。 +本教程演示了如何使用FreeType库绘制TrueType文本。这种方式灵活、可缩放并支持多种字符编码。然而,由于我们对每一个字形都生成并渲染了纹理,你的应用程序可能并不需要这么强大的功能。性能更好的位图字体也许是更可取的,因为对所有的字形我们只需要一个纹理。当然,最好的方式是结合这两种方式,动态生成包含所有字符字形的位图字体纹理,并用FreeType加载。这为渲染器节省了大量纹理切换的开销,并且根据字形的排列紧密程度也可以节省很多的性能开销。 -另一个使用FreeType字体的问题是字形纹理是对应着一个固定的字体大小的,因此直接对其放大就会出现锯齿边缘。此外,对字形进行旋转还会使它们看上去变得模糊。通过将每个像素设为最近的字形轮廓的像素,而不是直接设为实际栅格化的像素,可以减轻这些问题。这项技术被称为**有向距离场(Signed Distance Fields)**,Valve在几年前发表过一篇了[论文](http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf),探讨了他们通过这项技术来获得好得惊人的3D渲染效果。 \ No newline at end of file + + +另一个使用FreeType字体的问题是字形纹理是储存为一个固定的字体大小的,因此直接对其放大就会出现锯齿边缘。此外,对字形进行旋转还会使它们看上去变得模糊。这个问题可以通过储存每个像素距最近的字形轮廓的距离,而不是光栅化的像素颜色,来缓解。这项技术被称为有向距离场(Signed Distance Fields),Valve在几年前发表过一篇了[论文](http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf),讨论了他们通过这项技术来获得非常棒的3D渲染效果。 \ No newline at end of file diff --git a/glossary.md b/glossary.md index 5b22f9e..2fdba29 100644 --- a/glossary.md +++ b/glossary.md @@ -149,6 +149,16 @@ - Attachment:附件 - Render Buffer Object:渲染缓冲对象 +## 06-02 + +- Character:字符 +- Quad:四边形 +- Mesh:网格 +- Glyph:字形 +- Baseline:基线 +- Bitmap:位图 +- Metric:度量值 + ## 06-03-01 - Level:关卡 diff --git a/mkdocs.yml b/mkdocs.yml index 5150225..b5b0e76 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -52,7 +52,7 @@ pages: - SSAO: '05 Advanced Lighting/09 SSAO.md' - 实战: - 调试: '06 In Practice/01 Debugging.md' - - 文字渲染: '06 In Practice/02 Text Rendering.md' + - 文本渲染: '06 In Practice/02 Text Rendering.md' - 2D游戏: - Breakout: '06 In Practice/2D-Game/01 Breakout.md' - 准备工作: '06 In Practice/2D-Game/02 Setting up.md'