经典文本渲染:位图字体

数学函数早期的时候,渲染文本是通过选择一个需要的字体(Font)(或者自己创建一个),并提取这个字体中所有相关的字符,将它们放到一个单独的大纹理中来实现的。这样一张纹理叫做位图字体(Bitmap Font),它在纹理的预定义区域中包含了我们想要使用的所有字符。字体的这些字符被称为字形(Glyph)。每个字形都关联着一个特定的纹理坐标区域。当你想要渲染一个字符的时候,你只需要通过渲染这一块特定的位图字体区域到2D四边形上即可。



使用这种方式绘制文本有许多优势也有很多缺点。首先,它相对来说很容易实现,并且因为位图字体已经预光栅化了,它的效率也很高。然而,这种方式不够灵活。当你想要使用不同的字体时,你需要重新编译一套全新的位图字体,而且你的程序会被限制在一个固定的分辨率。如果你对这些文本进行缩放的话你会看到文本的像素边缘。

现代文本渲染:FreeType

FreeType是一个能够用于加载字体并将他们渲染到位图以及提供多种字体相关的操作的软件开发库。FreeType的真正吸引力在于它能够加载TrueType字体。它被用于Mac OS X、Java、PlayStation主机、Linux、Android等平台。很多游戏引擎,比如说unity也使用了freetype,来生成动态字体,参考这里

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

使用FreeType加载的每个字形没有相同的大小(不像位图字体那样)。使用FreeType生成的位图的大小恰好能包含这个字符可见区域。例如生成用于表示’.’的位图的大小要比表示’X’的小得多。因此,FreeType同样也加载了一些度量值来指定每个字符的大小和位置。下面这张图展示了FreeType对每一个字符字形计算的所有度量值。


每一个字形都放在一个水平的基准线(Baseline)上(即上图中水平箭头指示的那条线)。一些字形恰好位于基准线上(如’X’),而另一些则会稍微越过基准线以下(如’g’或’p’)(译注:即这些带有下伸部的字母,可以见这里)。这些度量值精确定义了摆放字形所需的每个字形距离基准线的偏移量,每个字形的大小,以及需要预留多少空间来渲染下一个字形。下面这个表列出了我们需要的所有属性。

属性 获取方式 生成位图描述
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 水平预留值,即原点到下一个字形原点的水平距离

根据上述api,生成对应的行文字:

#include <ft2build.h>
#include FT_FREETYPE_H 

void render()
{
    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/arial.ttf", 0, &face))  //加载字体
        std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;

    for (GLubyte 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;
        }
    }

    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, VBO);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); 
        
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glDrawArrays(DRAW_MODE, 0, 6);
    }
}

注意事项

  1. 1.我们把FreeType生成的位图存储在一个单通道的buffer中, 所以OpenGL根据位图宽高和buffer绑定给Texture2D
glTexImage2D(GL_TEXTURE_2D, 
            0, 
            GL_RED, 
            glyph->bitmap.width, 
            glyph->bitmap.rows, 
            0, 
            GL_RED, 
            GL_UNSIGNED_BYTE, 
            glyph->bitmap.buffer);

在OpenES平台上,是不支持GL_RED,需要使用GL_LUMINANCE, 否则是看不到字体显示的。具体参考这里

glTexImage2D(GL_TEXTURE_2D, 
            0, 
            GL_LUMINANCE, 
            glyph->bitmap.width, 
            glyph->bitmap.rows, 
            0, 
            GL_LUMINANCE, 
            GL_UNSIGNED_BYTE, 
            glyph->bitmap.buffer);
  1. 2.编译iOS平台下FreeType库,可能是一件非常痛苦的事儿。一直感觉文档不健全,网上看到的shell脚本也比较老。这里有一个现成的xcode static project,直接可以使用。

最后需要注意的是:需要分别编译模拟器和真机上两个版本的库, 然后使用lipo -create 合并成一个flat的lib