一、FreeType库
FreeType是一个能够提供多种字体相关的操作的软件开发库,往往使用它来做最简单的文字渲染:
OpenGL环境配置(超全整合版)FreeType库可以从这篇文章中的链接中下载到,也可以直接去他们的官方网站上获取
和其它环境配置方法一样,编译其builds文件夹的对应版本,并将include文件夹里面的所有文件全部放入老位置(C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.25.28610\include),编译成功后,应该就可以获得对应的dll和lib,dll放入老位置(C:\Windows\SysWOW64),lib放入老位置(C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.25.28610\lib\x86)
如果嫌编译麻烦,又是windows操作系统,可以直接从objs\Win32\Debug种获得已编译好的文件,你编译的结果也会在这里面
只要包含对应的头文件,就可以应用了:
#include <ft2build.h>
#include FT_FREETYPE_H
二、字形加载
TrueType字体(TTF):
新型数学字形描述技术,它用数学函数描述字体轮廓外形,含有字形构造、颜色填充、数字描述函数、流程条件控制、栅格处理控制、附加提示控制等指令。通过数学公式描述字体意味着可以轻易渲染不同大小的字形而不造成任何质量损失,这也是当下使用的主流字体,FreeType正可以加载TrueType字体

要加载字体,需要先初始化FreeType库,在这里你可以加载你字体的TTF文件:
- FT_Set_Pixel_Sizes(face, w, h):设置字体默认参数大小,最后两个参数为宽和高,如果宽填0则为通过高来动态计算
- glPixelStorei(GL_UNPACK_ALIGNMENT, 1):设置纹理解压对齐参数
OpenGL要求所有的纹理都是4字节对齐的,即纹理的大小永远是4字节的倍数,通常这并不会出现什么问题,因为大部分纹理的宽度都为4的倍数并/或每像素使用4个字节,但是对于字体纹理,往往需要支持任意宽度,并且只需要一个字节(用于表示透明度,当前片元显示/不显示),所以这里需要将纹理解压对齐参数设为1,这样才能确保不会有对齐问题(可能会引起段错误)
FT_Library ft;
if (FT_Init_FreeType(&ft))
std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl;
FT_Face face;
if (FT_New_Face(ft, "Fonts/Common.ttf", 0, &face))
std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;
FT_Set_Pixel_Sizes(face, 0, 48);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
一个比较麻烦的事情是:每个字母/文字都会有不同的大小为中心点位置,例如"g", ".", "X"这3个字符在显示相同大小时,实际位图的大小依旧不同,因此每个文字FreeType都需要为每一个字符去单独计算对应的属性,也称作为度量值:

大多数的字体都会在基准线之上,而对于存在下伸部的字体,就会有一部分越过基准线(例如p和g)
这些度量值精确定义了摆放字形所需的每个字形距离基准线的偏移量,每个字形的大小,以及需要预留多少空间来渲染下一个字形。下面这个表列出了暂时需要的所有属性
属性 | 获取方式 | 生成位图描述 |
---|---|---|
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像素) |
在需要渲染字符时,需要先加载字符字形并获取它的度量值,对于重复渲染的字体,往往需要将这些属性存储起来以节省效率:
truct Character
{
GLuint TextureID; // 字形纹理的ID
glm::ivec2 Size; // 字形大小
glm::ivec2 Bearing; // 从基准线到字形左部/顶部的偏移值
GLuint Advance; // 原点距下一个字形原点的距离
};
std::map<GLchar, Character> Characters;
如果只考虑ASCLL字符集,那就更好办了,直接将所有的127个字符离线处理下:
- FT_Load_Char(face, ch, FT_LOAD_RENDER):核心逻辑,将字形ch设置为激活字形以加载
代码中将纹理的internalFormat和format设置为GL_RED,上面也提到过:通过字形生成的位图是一个8位灰度图,它的每一个颜色都由一个字节来表示,因此需要将位图缓冲的每一字节都作为纹理的颜色值
for (GLubyte ch = 0; ch < 128; ch++)
{
if (FT_Load_Char(face, ch, FT_LOAD_RENDER))
{
std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
continue;
}
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, face->glyph->bitmap.width, face->glyph->bitmap.rows, 0, GL_RED, GL_UNSIGNED_BYTE, face->glyph->bitmap.buffer);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
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),
glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
face->glyph->advance.x
};
Characters.insert(std::pair<GLchar, Character>(ch, character));
}
glBindTexture(GL_TEXTURE_2D, 0);
处理完字形后顺便清理FreeType的资源:
FT_Done_Face(face);
FT_Done_FreeType(ft);
三、文字渲染
搞定了预处理后,就可以开始渲染文字了:着色器非常简单
在顶点着色器中,需要给一个投影矩阵,这里直接用最简单的正射投影矩阵,并将视口范围设定为投影的范围,这样你渲染出来的实际纹理位置就正对应了屏幕位置
在片段着色器中,需要给予一个字体颜色
#version 330 core
layout (location = 0) in vec4 vertex;
out vec2 TexIn;
uniform mat4 projection;
void main()
{
gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
TexIn = vertex.zw;
}
#version 330 core
in vec2 TexIn;
out vec4 color;
uniform sampler2D text;
uniform vec3 textColor;
void main()
{
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexIn).r);
color = vec4(textColor, 1.0) * sampled;
}
对于VAO和VBO,没有什么差别,不过这里只需要给VBO时分配足够的内存而暂时不去填充它,等到渲染字符的时候再用glBufferSubData来更新它的内存(别忘了要将内存类型设置为GL_DYNAMIC_DRAW),之所以预分配的大小是6 * 4,是因为每个2D四边形需要6个顶点,每个顶点又有一个4float向量(二维空间坐标xy和纹理坐标)
if (textVAO == 0)
{
glGenVertexArrays(1, &textVAO);
glGenBuffers(1, &textVBO);
glBindVertexArray(textVAO);
glBindBuffer(GL_ARRAY_BUFFER, textVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
别忘了传递参数到着色器,并且开启混合,关闭深度测试:
开启混合的作用正是为了显示字体,本质上,文字渲染就是判断一个矩形区域内,哪些像素点需要显示颜色:
右图就是未开启混合的渲染效果
glEnable(GL_BLEND);
glEnable(GL_PROGRAM_POINT_SIZE);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_DEPTH_TEST);
shader.Use();
glm::mat4 projection = glm::ortho(0.0f, static_cast<GLfloat>(WIDTH), 0.0f, static_cast<GLfloat>(HEIGHT));
glUniform3f(glGetUniformLocation(shader.Program, "textColor"), color.x, color.y, color.z);
glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
glActiveTexture(GL_TEXTURE1);
glUniform1i(glGetUniformLocation(shader.Program, "text"), 1);
glBindVertexArray(textVAO);
最后就是主体部分:步骤如下
- 计算当前字符四边形的原点坐标(posx, posy)和大小(w, h)
- 赋予这个四边形位置属性和纹理属性
- 更新四边形内容
- 渲染
如果不了解四边形位置和宽高的计算,可以参考上面度量值那张图
std::string::const_iterator c;
for (c = text.begin(); c != text.end(); c++)
{
Character ch = Characters[*c];
GLfloat xpos = x + ch.Bearing.x * scale;
GLfloat ypos = y - (ch.Size.y - ch.Bearing.y) * scale;
GLfloat w = ch.Size.x * scale;
GLfloat h = ch.Size.y * scale;
GLfloat vertices[6][4] =
{
{ xpos, ypos + h, 0.0, 0.0 },
{ xpos, ypos, 0.0, 1.0 },
{ xpos + w, ypos, 1.0, 1.0 },
{ xpos, ypos + h, 0.0, 0.0 },
{ xpos + w, ypos, 1.0, 1.0 },
{ xpos + w, ypos + h, 1.0, 0.0 }
};
glBindTexture(GL_TEXTURE_2D, ch.TextureID);
glBindBuffer(GL_ARRAY_BUFFER, textVBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
x += (ch.Advance >> 6) * scale;
}
如果没问题的话,就可以在任意地方显示文字了,而只需要更换ttf文件的路径,就可以实现字体的变更
四、扩展
有向距离场(Signed Distance Fields):
使用FreeType字体的问题是字形纹理是储存为一个固定的字体大小的,因此直接对其放大就会出现锯齿边缘。此外对字形进行旋转还会使它们看上去变得模糊,这可以通过储存每个像素距最近的字形轮廓的距离而不是光栅化的像素颜色来缓解,这项技术被称为有向距离场(Signed Distance Fields)
位图字体:
通俗易懂的讲解位图字体,就是直接将字符存储于图片,每一个字符都有一个实际的纹理图片,当你需要显示对应字符时直接加载对应的纹理,往往常用于显示数字1-9,字母A-Z,好处是可以实现各种艺术字效果,坏处就是样式和分辨率都被固定
在游戏设计中,伤害跳字,连招UI显示等往往用的都是位图字体
UI设计:
这个就不用说了,文字显示仅是UI设计的最简单的一部分,相当于HelloWorld级别,想要实现一个简单的UI框架都并非易事