此篇博文为个人OpenGL学习笔记,内容参考LearnOpenGL CNGLFW官方文档OpenGL 4 Reference Pages、OpenGL编程指南(原书第9版)

OpenGL本身并不包含任何的文本处理能力,所以OpenGL绘制文本到屏幕的相关内容需要自行定义。

FreeType是一个能够用于加载字体并将它们渲染到位图以及提供多种字体相关的操作的软件开发库。FreeType能够加载TrueType字体。

TrueType字体不是通过像素或其它不可缩放的方式进行定义而是通过数学公式(曲线的组合)进行定义。类似于矢量图像,光栅化后的字体图像可以根据需要的字体高度来生成。使用TrueType字体可以轻易渲染不同大小的字形而不造成任何质量损失。

TrueType会加载TrueType字体并为每个字体生成位图以及计算几个度量值(Metric)。可以提取出它生成的位图作为字形的纹理并使用这些度量值定位字符的字形。

要加载字体需要初始化FreeType库并将被加载字体加载为一个FreeType中的面(Face)。加载一个Windows/Fonts拷贝出来的TrueType字体文件arial.ttf

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, "src/font/arial.ttf", 0, &face)) {
    std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;
}

这些FreeType函数出错将返回一个非零整数值。

加载后定义字体大小:

FT_Set_Pixel_Sizes(face, 0, 48);

将宽度设置为0标示要从字体面通过给定的高度动态计算出字形的宽度。

一个FreeType面中包含了一个字形的集合,可以调用FT_Load_Char()函数将其中一个字形设置为激活字形,加载字符字形X

if (FT_Load_Char(face, 'X', FT_LOAD_RENDER)) {
    std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
}

可以通过face->glyph->bitmap访问此位图。

使用FreeType加载的每个字形没有相同的大小。使用FreeType生成的位图的大小恰好能包含这个字符的可见区域。例如生成用于表示'.'的位图的大小要比表示'X'的小得多。所以FreeType同样也加载了一些度量值来指定每个字符的大小和位置。

FreeType对每一个字符字形计算的所有度量值

每个字形斗方在一个水平的基准线(Baseline)上(图中水平箭头指示的线)。一些字形恰好位于基准线上(如'X'),而另一些则会稍微越过基准线以下(如'g'或'p')。这些度量值精确定义了摆放字形所需的每个字形距离基准线的偏移量,每个字形的大小以及需要预留多少空间来渲染下一个字形。需要的所有属性:

属性获取方式生成位图描述
widthface->glyph->bitmap.width位图宽度(像素)
heightface->glyph->bitmap.rows位图高度(像素)
bearingXface->glyph->bitmap_left水平距离,即位图相对于原点的水平位置(像素)
bearingYface->glyph->bitmap_top垂直距离,即位图相对于基准线的垂直位置(像素)
advanceface->glyph->advance.x水平预留值,即原点到下一个字形原点的水平距离(单位:1/64像素)

在需要渲染字符时可以加载一个字符字形获取它的度量值并生成一个纹理,但每一帧都这样会很没效率。所以定义一个结构体并将这些结构体存储在std::map中:

struct Character {
    unsigned int texture_id;
    glm::vec2 size;
    glm::vec2 bearing;
    long advance;
};

std::map<unsigned char, Character> characters;

生成ASCII字符集的前128个字符,对每一个字符生成一个纹理并保存相关数据到Character结构体中再把结构体添加到characters映射表中。

遍历ASCII集中128个字符并获取对应的字符字形。对每一个字符生成纹理设置选项并储存其度量值:

for (unsigned char c = 0; c < 128; ++c) {
    if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
        std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
        continue;
    }
    unsigned texture_id;
    glGenTextures(1, &texture_id);
    glBindTexture(GL_TEXTURE_2D, texture_id);
    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);
    characters.insert({ c, {texture_id, glm::vec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), glm::vec2(face->glyph->bitmap_left, face->glyph->bitmap_top), face->glyph->advance.x} });
}

在遍历字符字形时将纹理的internalFormatformat设置为了GL_RED。通过字形生成的位图是8位灰度图,它每个颜色都由一个字节表示,所以需要将位图缓冲的每一字节都作为纹理的颜色值。这是通过创建一个特殊的纹理进行实现,此纹理每一字节都对应着纹理颜色的红色分量(颜色向量的第一个字节)。如果使用一个字节表示纹理的颜色则需要注意OpenGL要求所有的纹理都是4字节对齐,即纹理的大小永远是4字节的倍数,但现在每个像素只用一个字节它可以是任意的宽度,通过将纹理对齐参数设为1才能确保不会有对齐问题(可能造成段错误)。

禁用字节对齐限制:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

最后清理FreeType资源:

FT_Done_Face(face);
FT_Done_FreeType(ft);

渲染字形的顶点着色器:

#version 460 core
layout (location = 0) in vec4 vertex;

out vec2 f_tex_coords;

uniform mat4 projection;

void main() {
    gl_Position = projection * vec4(vertex.xy, 0.0f, 1.0f);
    f_tex_coords = vertex.zw;
}

把位置和纹理的纹理坐标数据合起来存在一个vec4变量中。顶点着色器把位置坐标和一个投影矩阵相乘并将纹理坐标片段传递给片段着色器:

#version 460 core
in vec2 f_tex_coords;

out vec4 color;

uniform sampler2D text;
uniform vec3 text_color;

void main() {
    vec4 sampled = vec4(1.0f, 1.0f, 1.0f, texture(text, f_tex_coords).r);
    color = vec4(text_color, 1.0f) * sampled;
}

片段着色器有两个uniform变量:text是单颜色通道的字形图纹理,text_color是颜色,它可以用来调整文本的最终颜色。首先从位图中采样颜色值,由于纹理数据仅存储红色分量所以就采样纹理的r分量作为取样的alpha值。通过变换颜色的alpha值最终的颜色在字形背景颜色上会是透明的,而真正的字符像素上是不透明的。将RGB颜色与text_coloruniform变量相乘变换文本颜色。

启用混合:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 

对于投影矩阵则使用正射投影矩阵。文本渲染(通常)不需要透视,使用正射投影同样允许在屏幕坐标系中设定所有的顶点坐标:

glm::mat4 projection = glm::ortho(0.0f, 800.0f, 0;0f, 600.0f);

创建VBO和VAO来渲染四边形。初始化VBO时分配足够的内存,这样可以在渲染字体时再更新vbo内存:

unsigned int vbo, vao;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, 6 * 4 * sizeof(GLfloat), NULL, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

每个2D四边形需要6个顶点,每个顶点由一个4float向量(一个纹理坐标和一个顶点坐标)组成,所以将VBO的内存分配$6 \times 4$个float的大小。由于在绘制字符时经常更新VBO的内存,所以将内存类型设置为GL_DYNAMIC_DRAW

要渲染一个字符从characters映射表中取出对应的Character结构体并根据字符的度量值计算四边形的维度,根据四边形的维度动态计算出6个描述四边形的顶点并使用glBufferSubData()函数更新VBO所管理内存的内容。

创建一个RenderText()的函数渲染一个字符串:

void RenderText(Shader& shader, std::string text, float x, float y, float scale, glm::vec3 color) {
    shader.Use();
    shader.SetVec3("text_color", color);
    glActiveTexture(GL_TEXTURE0);
    glBindVertexArray(vao);

    for (char c : text) {
        Character ch = characters[c];

        float x_pos = x + ch.bearing.x * scale;
        float y_pos = y - (ch.size.y - ch.bearing.y) * scale;

        float w = ch.size.x * scale;
        float h = ch.size.y * scale;

        float vertices[6][4] = {
            { x_pos, y_pos + h, 0.0f, 0.0f },
            { x_pos, y_pos, 0.0f, 1.0f },
            { x_pos + w, y_pos, 1.0f, 1.0f },
            { x_pos, y_pos + h, 0.0f, 0.0f },
            { x_pos + w, y_pos, 1.0f, 1.0f },
            { x_pos + w, y_pos + h, 1.0f, 0.0f }
        };

        glBindTexture(GL_TEXTURE_2D, ch.texture_id);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glDrawArrays(GL_TRIANGLES, 0, 6);

        x += (ch.advance >> 6)* scale;
    }

    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);
}

首先计算出四边形的原点坐标(x_pos和y_pos)和它的大小(长宽,w和h)并生辰6个顶点形成四边形(每个度量值都使用scale)进行了缩放。接着更新了VBO的内容并渲染四边形。

其中:

float y_pos = y - (ch.size.y - ch.bearing.y) * scale;

一些字符(如'p'或'q')需要被渲染到基准线以下,因此字形四边形也应该被摆放在RenderText()的y值以下。y_pos的偏移量可以从字形的度量值中得出:

偏移量的计算

通过字形的高度减去bearing.y来计算偏移量。对于那些正好位于基准线上的字符(如'X'),这个值正好是0.0。而对于那些超出基准线的字符(如'g'或'j'),这个值则是正的。

调用渲染函数渲染字符串:

RenderText(shader, "I love you 3000", 25.0f, 25.0f, 1.0f, glm::vec3(0.5, 0.8f, 0.2f));
RenderText(shader, "tony5t4rk.cn", 540.0f, 570.0f, 0.5f, glm::vec3(0.3, 0.7f, 0.9f));

完整代码:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <map>
#include "shader.h"
#include <ft2build.h>
#include FT_FREETYPE_H

const unsigned int window_width = 800;
const unsigned int window_height = 600;

void ProcessInput(GLFWwindow* window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, true);
    }
}

struct Character {
    unsigned int texture_id;
    glm::vec2 size;
    glm::vec2 bearing;
    long advance;
};

std::map<unsigned char, Character> characters;

unsigned int vbo, vao;

void RenderText(Shader& shader, std::string text, float x, float y, float scale, glm::vec3 color) {
    shader.Use();
    shader.SetVec3("text_color", color);
    glActiveTexture(GL_TEXTURE0);
    glBindVertexArray(vao);

    for (char c : text) {
        Character ch = characters[c];

        float x_pos = x + ch.bearing.x * scale;
        float y_pos = y - (ch.size.y - ch.bearing.y) * scale;

        float w = ch.size.x * scale;
        float h = ch.size.y * scale;

        float vertices[6][4] = {
            { x_pos, y_pos + h, 0.0f, 0.0f },
            { x_pos, y_pos, 0.0f, 1.0f },
            { x_pos + w, y_pos, 1.0f, 1.0f },
            { x_pos, y_pos + h, 0.0f, 0.0f },
            { x_pos + w, y_pos, 1.0f, 1.0f },
            { x_pos + w, y_pos + h, 1.0f, 0.0f }
        };

        glBindTexture(GL_TEXTURE_2D, ch.texture_id);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glDrawArrays(GL_TRIANGLES, 0, 6);

        x += (ch.advance >> 6)* scale;
    }

    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);
}

int main() {
    if (!glfwInit()) {
        std::cout << "Failed to initialize GLFW" << std::endl;
        return -1;
    }
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(window_width, window_height, "Hello OpenGL", NULL, NULL);
    if (window == NULL) {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    Shader shader("font_shader.vs", "font_shader.fs");
    glm::mat4 projection_matrix = glm::ortho(0.0f, (float)window_width, 0.0f, (float)window_height);
    shader.Use();
    shader.SetMat4("projection", projection_matrix);

    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, "src/font/arial.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);

    for (unsigned char c = 0; c < 128; ++c) {
        if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
            std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
            continue;
        }
        unsigned texture_id;
        glGenTextures(1, &texture_id);
        glBindTexture(GL_TEXTURE_2D, texture_id);
        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);
        characters.insert({ c, {texture_id, glm::vec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), glm::vec2(face->glyph->bitmap_left, face->glyph->bitmap_top), face->glyph->advance.x} });
        glBindTexture(GL_TEXTURE_2D, 0);
    }

    FT_Done_Face(face);
    FT_Done_FreeType(ft);

    glGenVertexArrays(1, &vao);
    glGenBuffers(1, &vbo);
    glBindVertexArray(vao);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, 6 * 4 * sizeof(float), NULL, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    while (!glfwWindowShouldClose(window)) {
        ProcessInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        RenderText(shader, "I love you 3000", 25.0f, 25.0f, 1.0f, glm::vec3(0.5, 0.8f, 0.2f));
        RenderText(shader, "tony5t4rk.cn", 540.0f, 570.0f, 0.5f, glm::vec3(0.3, 0.7f, 0.9f));

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

运行效果:

运行效果

Last modification:March 28th, 2020 at 09:52 pm
如果觉得我的文章对你有用,请随意赞赏