Opengl ES之文字渲染

1,176 阅读13分钟

本文正在参加「金石计划」

前言

自此已是我关于Opengl ES系列入门教程的第16篇文章了,虽然写的不咋的,文章产出量也不高,但是这个系列从2022年8月底开始持续到现在也坚持了比较久, 每一篇文章都是经过自己的精心调试通过后再写文总结发布,这个过程耗费了我很多的私人时间,其实一开始想写这个系列就是为了想看看自己不能系统性地把一件事情做好, 现在看来依然比较难,因为自己平常除了工作以外,偶尔也会去峡谷浪一浪,本身自己就是个不靠谱的宝爸,所以经常周末还得往老家跑。

目前这个系列还有滤镜、转场动画等一些内容没有写完,大部分写博文的童鞋都知道,这大概就是一个用爱发电的过程,没有利益,也没有营收,离谱一点写的不好,可能还会被喷... 本来打算是这个系列完善后再放出源码的,但是有不少好学的童鞋加了V信索要,同时也不知道后续这个系列自己还会不会继续完善下去,因此这次就把这个系列的源码放出来啦,仅供参考。

所谓纸上得来总觉浅,绝知此事要躬行,其实在之前的博文中,我都贴上了关键的代码,如果真有时间又想学习的话还是建议自己亲手敲一遍学得牢,理解得透切。

今天的主要内容是使用Opengl ES搭配FreeType进行文字渲染。

FreeType

文字渲染是做音视频开发逃不掉的一个需求,实际上 OpenGL 并没有定义渲染文字的方式,但是我们可以通过纹理贴图的方式实现文字渲染,就是将带有文字的图像上传到纹理,然后进行纹理贴图。 因此在Android平台是使用Opengl ES进行文字渲染就有两个不同的方案,一个是使用Canvas绘制文字生产Bitmap,然后将带文字的Bitmap进行渲染;另外一个是使用跨平台的字体解析库FreeType实现。

FreeType是一个基于 C 语言实现的用于文字渲染的跨平台开源库,它小巧、高效、高度可定制,主要用于加载字体并将其渲染到位图,支持多种字体的相关操作。

FreeType 官网地址:

https://www.freetype.org/

更多关于FreeType的介绍以及使用文档请自行参考官网教程。

FreeType编译

一贯原则,要将一个跨平台C/C++库移植到Android,首先第一步就是先进行交叉编译,对交叉编译不了解的童鞋们请参考之前的文章:

音视频学习之NDK交叉编译基础

FreeType很贴心地为我们提供了configure文件,因此我们只需配置一下NDK的相关环境即可编译成功,更多关于configure可配置参数, 可通过命令行configure --help查看。

下面是笔者在mac上编译FreeType的一个脚本:

#!/bin/bash

# 这里需要配置你的NDK路径
NDK_PATH="/Users/fly/Documents/Android/SDK/ndk/21.4.7075529/"
# 注意我这里是苹果系统,如果是Linux系统是不一样的
HOST_PLATFORM="darwin-x86_64"

COMMON_OPTIONS="
    --with-zlib=no
    --with-bzip2=no
    --with-png=no
    --with-harfbuzz=no
    --with-brotli=no
    --with-sysroot=${SYSROOT}
    "
TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin"
SYSROOT="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/sysroot"

make clean

FOLDER=`pwd`/android-libs/arm64-v8a
mkdir -p ${FOLDER}
CC=${TOOLCHAIN_PREFIX}/aarch64-linux-android21-clang \
./configure \
    --prefix=${FOLDER} \
    --libdir=${FOLDER} \
    --host=aarch64-linux-android \
    --enable-debug \
    ${COMMON_OPTIONS}

make -j4 && make install

编译成功后即可将静态库引入到Android Studio中使用了,引入的步骤也很简单,主要是配置一下头文件的搜索路径和动态库的查找路径即可,详情可参考之前的博文:

将ffmpeg引入到Android Studio工程中

FreeType的使用

有关FreeType的使用,官网有比较详细的介绍:freetype.org/freetype2/d…

首先引入的头文件是:

#include "ft2build.h"
#include <freetype/ftglyph.h>

在FreeType中每个字体库加载后就是一个叫做FT_Face的东西,后续通过FT_Face这个结构图可以获取到文字的一系列信息,比如图像宽高等。

首先使用FT_Init_FreeType进行初始化,然后通过函数FT_New_Face得到一个FT_Face。

下面这段代码的意思就是将128个ASCII,并将相关字体图像信息缓存起来:

void TextRenderOpengl::LoadFacesByASCII() {
    // FreeType
    FT_Library ft;
    FT_Face face;
    do {
        if (FT_Init_FreeType(&ft)) {
            LOGD("FT_Init_FreeType fail");
            break;
        }
        // FONT_FILE_PATH替换成自己的字体文件路径
        if (FT_New_Face(ft, FONT_FILE_PATH, 0, &face)) {
            LOGD("FT_New_Face fail");
            break;
        }

        // 设置文字的大小,此函数设置了字体面的宽度和高度
        // 将宽度值设为0表示我们要从字体面通过给出的高度中动态计算出字形的宽度。
        FT_Set_Pixel_Sizes(face, 0, 90);
        // 字节对齐
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

        // Load first 128 characters of ASCII set
        for (unsigned char c = 0; c < 128; c++) {
            // Load character glyph
            if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
                LOGD("FT_Load_Char: Failed to load Glyph");
                continue;
            }
            // 纹理
            GLuint texture;
            glGenTextures(1, &texture);
            glBindTexture(GL_TEXTURE_2D, texture);
            glTexImage2D(
                    GL_TEXTURE_2D,
                    0,
                    GL_LUMINANCE,
                    face->glyph->bitmap.width,
                    face->glyph->bitmap.rows,
                    0,
                    GL_LUMINANCE,
                    GL_UNSIGNED_BYTE,
                    face->glyph->bitmap.buffer
            );
            // Set texture options
            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);
            // Now store character for later use
            Character character = {
                    texture,
                    glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
                    glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
                    static_cast<GLuint>(face->glyph->advance.x)
            };
            m_Characters.insert(std::pair<GLint, Character>(c, character));
        }
        break;
    } while (0);
    glBindTexture(GL_TEXTURE_2D, 0);
    // 字体库释放
    FT_Done_Face(face);
    FT_Done_FreeType(ft);
}

使用Opengl ES进行渲染

首先我们的偏远着色器是:

#version 300 es
precision mediump float;
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
uniform vec3 u_textColor;
void main()
{
    vec4 color = vec4(1.0, 1.0, 1.0, texture(ourTexture, TexCoord).r);
    FragColor = vec4(u_textColor, 1.0) * color;
}

核心逻辑就是获取本身字体纹理的一个单颜色通道,然后通过变量u_textColor这个变量重新组合改变字体的渲染颜色。

既然前面我们已经获取了每个文字的图像数据,那么将这些文字的图像进行纹理贴图即可渲染出来,因为我们不是渲染一个文字,可能是渲染一行或者多个文字, 因此我们封装一个渲染ASCII的方法:


/**
 *
 * @param text 需要渲染的文字
 * @param x 归一化坐标中文字渲染x的起点  [-1,1]
 * @param y 归一化坐标中文字渲染y的起点  [-1,1]
 * @param scale 字体缩放  这个也可以在顶点坐标中做统一的变换使用
 * @param color 字体颜色
 * @param viewport 视窗大小
 */
void TextRenderOpengl::RenderASCIIText(std::string text, GLfloat x, GLfloat y, GLfloat scale,
                                       glm::vec3 color, glm::vec2 viewport) {

    // 激活合适的渲染状态
    glUseProgram(program);

    // 纹理坐标都是一样的,先赋值纹理坐标
    // 纹理坐标
    glEnableVertexAttribArray(textureHandle);
    glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 0, TEXTURE_COORD);

    glUniform3f(u_textColorHandle, color.x, color.y, color.z);
    // 对文本中的所有字符迭代
    std::string::const_iterator c;
    x *= viewport.x;
    y *= viewport.y;
    for (c = text.begin(); c != text.end(); c++) {
        Character ch = m_Characters[*c];

        GLfloat xpos = x + ch.bearing.x * scale;
        GLfloat ypos = y - (ch.size.y - ch.bearing.y) * scale;

        // 归一化
        xpos /= viewport.x;
        ypos /= viewport.y;

        // 字体贴图缩放后的宽高
        GLfloat w = ch.size.x * scale;
        GLfloat h = ch.size.y * scale;

        // 宽高也归一化
        w /= viewport.x;
        h /= viewport.y;

        LOGD("TextRenderSample::RenderText [xpos,ypos,w,h]=[%f, %f, %f, %f], ch.advance >> 6 = %d",
             xpos, ypos, w, h, ch.advance >> 6);

        // 当前字符的顶点坐标
        GLfloat VERTICES[] = {
                xpos + w, ypos,
                xpos + w, ypos + h,
                xpos, ypos,
                xpos, ypos + h
        };

        /**
         * size 几个数字表示一个点,显示是两个数字表示一个点
         * normalized 是否需要归一化,不用,这里已经归一化了
         * stride 步长,连续顶点之间的间隔,如果顶点直接是连续的,也可填0
         */
        // 启用顶点数据
        glEnableVertexAttribArray(positionHandle);
        glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 0, VERTICES);

        // 在方块上绘制字形纹理
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, ch.textureID);
        glUniform1i(textureSampler, 0);
        // 绘制方块
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        // 更新位置到下一个字形的原点,注意单位是1/64像素
        x += (ch.advance >> 6) * scale; //(2^6 = 64)
    }
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);

    // 禁用顶点
    glDisableVertexAttribArray(positionHandle);
    glDisableVertexAttribArray(textureHandle);

}

核心思想就是归一化以及动态变更每个文字的顶点坐标和纹理坐标,记住一点就是顶点坐标决定绘制的的位置,纹理坐标决定绘制纹理图的那一部分内容即可。

FreeType渲染中文

自此我们渲染英文以及一些常用的标点字符是没有问题,但是如果我们要渲染中文就不行了,我们尝试使用我们封装的函数RenderASCIIText渲染中文就会发现是没有效果的。

与 ASCII 码不同的是,中文字符采用 2 字节的 Unicode 编码,所以在加载字体之前,我们首先需要设置编码类型:

FT_Select_Charmap(face, ft_encoding_unicode);

另外,还需要注意的一点是中文字符串需要采用宽字符wchar_t

下面是加载Unicode编码字体的实例代码:

// 注意你的字体库要支持中文,如果字体库不支持中文的话渲染是没有效果的,拿到的纹理图的宽高都是0
void TextRenderOpengl::LoadFacesByUnicode(const wchar_t *text, int size) {
    // FreeType
    FT_Library ft;
    FT_Face face;
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    // 因为FT_Library 要释放,所以使用do while
    do {
        // All functions return a value different than 0 whenever an error occurred
        if (FT_Init_FreeType(&ft)) {
            // todo 提示
            LOGD("LoadFacesByUnicode -----FT_Init_FreeType fail");
            break;
        }

        if (FT_New_Face(ft, FONT_FILE_PATH, 0, &face)) {
            // todo
            LOGD("LoadFacesByUnicode -----FT_New_Face fail");
            break;
        }

        // Set size to load glyphs as
        FT_Set_Pixel_Sizes(face, 0, 96);
        // 指定使用unicode
        FT_Select_Charmap(face, ft_encoding_unicode);
        for (int i = 0; i < size; ++i) {
            //int index =  FT_Get_Char_Index(face,unicodeArr[i]);
            if (FT_Load_Glyph(face, FT_Get_Char_Index(face, text[i]), FT_LOAD_DEFAULT)) {
                continue;
            }

            FT_Glyph glyph;
            FT_Get_Glyph(face->glyph, &glyph);

            //Convert the glyph to a bitmap.
            FT_Glyph_To_Bitmap(&glyph, ft_render_mode_normal, 0, 1);
            FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph) glyph;

            //This reference will make accessing the bitmap easier
            FT_Bitmap &bitmap = bitmap_glyph->bitmap;

            LOGD("LoadFacesByUnicode -----text[i]:%d",text[i]);
            LOGD("LoadFacesByUnicode -----w:%d,h:%d",bitmap.width,bitmap.rows);

            // Generate texture
            GLuint texture;
            glGenTextures(1, &texture);
            glBindTexture(GL_TEXTURE_2D, texture);
            glTexImage2D(
                    GL_TEXTURE_2D,
                    0,
                    GL_LUMINANCE,
                    bitmap.width,
                    bitmap.rows,
                    0,
                    GL_LUMINANCE,
                    GL_UNSIGNED_BYTE,
                    bitmap.buffer
            );
            // Set texture options
            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);
            // Now store character for later use
            Character character = {
                    texture,
                    glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
                    glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
                    static_cast<GLuint>((glyph->advance.x / MAX_SHORT_VALUE) << 6)
            };
            m_Characters.insert(std::pair<GLint, Character>(text[i], character));
            LOGD("LoadFacesByUnicode -----success");
        }
        break;
    } while (0);
    glBindTexture(GL_TEXTURE_2D, 0);
    // Destroy FreeType once we're finished
    FT_Done_Face(face);
    FT_Done_FreeType(ft);
}

完整的字体渲染代码:

/**
 * 本教程首发于公众号"思想觉悟"
 */
#include "EBOOpengl.h"


#include "TextRenderOpengl.h"
#include "ft2build.h"
#include <freetype/ftglyph.h>
#include "utils/Log.h"

// 顶点着色器
static const char *ver = "#version 300 es\n"
                         "in vec4 aPosition;\n"
                         "in vec2 aTexCoord;\n"
                         "out vec2 TexCoord;\n"
                         "void main() {\n"
                         "  TexCoord = aTexCoord;\n"
                         "  gl_Position = aPosition;\n"
                         "}";

// 片元着色器
static const char *fragment = "#version 300 es\n"
                              "precision mediump float;\n"
                              "out vec4 FragColor;\n"
                              "in vec2 TexCoord;\n"
                              "uniform sampler2D ourTexture;\n"
                              "uniform vec3 u_textColor;\n"
                              "void main()\n"
                              "{\n"
                              "    vec4 color = vec4(1.0, 1.0, 1.0, texture(ourTexture, TexCoord).r);\n"
                              "    FragColor = vec4(u_textColor, 1.0) * color;\n"
                              "}";


// 贴图纹理坐标(参考手机屏幕坐标系统,原点在左上角)
const static GLfloat TEXTURE_COORD[] = {
        1.0f, 1.0f, // 右下
        1.0f, 0.0f, // 右上
        0.0f, 1.0f, // 左下
        0.0f, 0.0f // 左上
};

//字体路径,写死了
const static char *FONT_FILE_PATH = "/data/user/0/com.fly.opengles.learn/files/jianti.ttf";

static const int MAX_SHORT_VALUE = 65536;

void TextRenderOpengl::LoadFacesByASCII() {
    // FreeType
    FT_Library ft;
    FT_Face face;
    do {
        if (FT_Init_FreeType(&ft)) {
            LOGD("FT_Init_FreeType fail");
            break;
        }
        // FONT_FILE_PATH替换成自己的字体文件路径
        if (FT_New_Face(ft, FONT_FILE_PATH, 0, &face)) {
            LOGD("FT_New_Face fail");
            break;
        }

        // 设置文字的大小,此函数设置了字体面的宽度和高度
        // 将宽度值设为0表示我们要从字体面通过给出的高度中动态计算出字形的宽度。
        FT_Set_Pixel_Sizes(face, 0, 90);
        // 字节对齐
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

        // Load first 128 characters of ASCII set
        for (unsigned char c = 0; c < 128; c++) {
            // Load character glyph
            if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
                LOGD("FT_Load_Char: Failed to load Glyph");
                continue;
            }
            // 纹理
            GLuint texture;
            glGenTextures(1, &texture);
            glBindTexture(GL_TEXTURE_2D, texture);
            glTexImage2D(
                    GL_TEXTURE_2D,
                    0,
                    GL_LUMINANCE,
                    face->glyph->bitmap.width,
                    face->glyph->bitmap.rows,
                    0,
                    GL_LUMINANCE,
                    GL_UNSIGNED_BYTE,
                    face->glyph->bitmap.buffer
            );
            // Set texture options
            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);
            // Now store character for later use
            Character character = {
                    texture,
                    glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
                    glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
                    static_cast<GLuint>(face->glyph->advance.x)
            };
            m_Characters.insert(std::pair<GLint, Character>(c, character));
        }
        break;
    } while (0);
    glBindTexture(GL_TEXTURE_2D, 0);
    // 字体库释放
    FT_Done_Face(face);
    FT_Done_FreeType(ft);
}

// 注意你的字体库要支持中文,如果字体库不支持中文的话渲染是没有效果的,拿到的纹理图的宽高都是0
void TextRenderOpengl::LoadFacesByUnicode(const wchar_t *text, int size) {
    // FreeType
    FT_Library ft;
    FT_Face face;
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    // 因为FT_Library 要释放,所以使用do while
    do {
        // All functions return a value different than 0 whenever an error occurred
        if (FT_Init_FreeType(&ft)) {
            // todo 提示
            LOGD("LoadFacesByUnicode -----FT_Init_FreeType fail");
            break;
        }

        if (FT_New_Face(ft, FONT_FILE_PATH, 0, &face)) {
            // todo
            LOGD("LoadFacesByUnicode -----FT_New_Face fail");
            break;
        }

        // Set size to load glyphs as
        FT_Set_Pixel_Sizes(face, 0, 96);
        // 指定使用unicode
        FT_Select_Charmap(face, ft_encoding_unicode);
        for (int i = 0; i < size; ++i) {
            //int index =  FT_Get_Char_Index(face,unicodeArr[i]);
            if (FT_Load_Glyph(face, FT_Get_Char_Index(face, text[i]), FT_LOAD_DEFAULT)) {
                continue;
            }

            FT_Glyph glyph;
            FT_Get_Glyph(face->glyph, &glyph);

            //Convert the glyph to a bitmap.
            FT_Glyph_To_Bitmap(&glyph, ft_render_mode_normal, 0, 1);
            FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph) glyph;

            //This reference will make accessing the bitmap easier
            FT_Bitmap &bitmap = bitmap_glyph->bitmap;

            LOGD("LoadFacesByUnicode -----text[i]:%d",text[i]);
            LOGD("LoadFacesByUnicode -----w:%d,h:%d",bitmap.width,bitmap.rows);

            // Generate texture
            GLuint texture;
            glGenTextures(1, &texture);
            glBindTexture(GL_TEXTURE_2D, texture);
            glTexImage2D(
                    GL_TEXTURE_2D,
                    0,
                    GL_LUMINANCE,
                    bitmap.width,
                    bitmap.rows,
                    0,
                    GL_LUMINANCE,
                    GL_UNSIGNED_BYTE,
                    bitmap.buffer
            );
            // Set texture options
            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);
            // Now store character for later use
            Character character = {
                    texture,
                    glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
                    glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
                    static_cast<GLuint>((glyph->advance.x / MAX_SHORT_VALUE) << 6)
            };
            m_Characters.insert(std::pair<GLint, Character>(text[i], character));
            LOGD("LoadFacesByUnicode -----success");
        }
        break;
    } while (0);
    glBindTexture(GL_TEXTURE_2D, 0);
    // Destroy FreeType once we're finished
    FT_Done_Face(face);
    FT_Done_FreeType(ft);
}

TextRenderOpengl::TextRenderOpengl() {

    initGlProgram(ver, fragment);
    positionHandle = glGetAttribLocation(program, "aPosition");
    textureHandle = glGetAttribLocation(program, "aTexCoord");
    textureSampler = glGetUniformLocation(program, "ourTexture");
    u_textColorHandle = glGetUniformLocation(program, "u_textColor");
    LoadFacesByASCII();
}

TextRenderOpengl::~TextRenderOpengl() noexcept {

}

void TextRenderOpengl::onDraw() {
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    //禁用byte-alignment限制 使用一个字节对齐
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    // 打开混合设置
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    // 渲染ASCII字符
    RenderASCIIText("Abcdef%h*!@#$", -0.9f, 0.2f, 3, glm::vec3(0.5, 0.5, 0.5),
                    glm::vec2(eglHelper->viewWidth, eglHelper->viewHeight));

    const wchar_t text[] = L"我看看中文的渲染";
    RenderUnicodeText(text,sizeof(text)/sizeof(text[0]),-0.9f,-0.4f,2,glm::vec3(0.5,0.5,0.5),glm::vec2(eglHelper->viewWidth,eglHelper->viewHeight));

    if (nullptr != eglHelper) {
        eglHelper->swapBuffers();
    }
}


void TextRenderOpengl::RenderUnicodeText(const wchar_t* text, int textLen, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color, glm::vec2 viewport){

    LoadFacesByUnicode(text,textLen);

    // 以下代码基本和RenderASCIIText一样的,只是字体库加载的方式变了而已

    // 激活合适的渲染状态
    glUseProgram(program);

    // 纹理坐标都是一样的,先赋值纹理坐标
    // 纹理坐标
    glEnableVertexAttribArray(textureHandle);
    glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 0, TEXTURE_COORD);

    glUniform3f(u_textColorHandle, color.x, color.y, color.z);
    x *= viewport.x;
    y *= viewport.y;
    for (int i = 0; i < textLen; ++i){
        Character ch = m_Characters[text[i]];

        GLfloat xpos = x + ch.bearing.x * scale;
        GLfloat ypos = y - (ch.size.y - ch.bearing.y) * scale;

        // 归一化
        xpos /= viewport.x;
        ypos /= viewport.y;

        // 字体贴图缩放后的宽高
        GLfloat w = ch.size.x * scale;
        GLfloat h = ch.size.y * scale;

        // 宽高也归一化
        w /= viewport.x;
        h /= viewport.y;

        LOGD("TextRenderSample::RenderUnicodeText [xpos,ypos,w,h]=[%f, %f, %f, %f], ch.advance >> 6 = %d",
             xpos, ypos, w, h, ch.advance >> 6);

        // 当前字符的顶点坐标
        GLfloat VERTICES[] = {
                xpos + w, ypos,
                xpos + w, ypos + h,
                xpos, ypos,
                xpos, ypos + h
        };

        /**
         * size 几个数字表示一个点,显示是两个数字表示一个点
         * normalized 是否需要归一化,不用,这里已经归一化了
         * stride 步长,连续顶点之间的间隔,如果顶点直接是连续的,也可填0
         */
        // 启用顶点数据
        glEnableVertexAttribArray(positionHandle);
        glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 0, VERTICES);

        // 在方块上绘制字形纹理
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, ch.textureID);
        glUniform1i(textureSampler, 0);
        // 绘制方块
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        // 更新位置到下一个字形的原点,注意单位是1/64像素
        x += (ch.advance >> 6) * scale; //(2^6 = 64)
    }
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);

    // 禁用顶点
    glDisableVertexAttribArray(positionHandle);
    glDisableVertexAttribArray(textureHandle);
}

/**
 *
 * @param text 需要渲染的文字
 * @param x 归一化坐标中文字渲染x的起点  [-1,1]
 * @param y 归一化坐标中文字渲染y的起点  [-1,1]
 * @param scale 字体缩放  这个也可以在顶点坐标中做统一的变换使用
 * @param color 字体颜色
 * @param viewport 视窗大小
 */
void TextRenderOpengl::RenderASCIIText(std::string text, GLfloat x, GLfloat y, GLfloat scale,
                                       glm::vec3 color, glm::vec2 viewport) {

    // 激活合适的渲染状态
    glUseProgram(program);

    // 纹理坐标都是一样的,先赋值纹理坐标
    // 纹理坐标
    glEnableVertexAttribArray(textureHandle);
    glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 0, TEXTURE_COORD);

    glUniform3f(u_textColorHandle, color.x, color.y, color.z);
    // 对文本中的所有字符迭代
    std::string::const_iterator c;
    x *= viewport.x;
    y *= viewport.y;
    for (c = text.begin(); c != text.end(); c++) {
        Character ch = m_Characters[*c];

        GLfloat xpos = x + ch.bearing.x * scale;
        GLfloat ypos = y - (ch.size.y - ch.bearing.y) * scale;

        // 归一化
        xpos /= viewport.x;
        ypos /= viewport.y;

        // 字体贴图缩放后的宽高
        GLfloat w = ch.size.x * scale;
        GLfloat h = ch.size.y * scale;

        // 宽高也归一化
        w /= viewport.x;
        h /= viewport.y;

        LOGD("TextRenderSample::RenderText [xpos,ypos,w,h]=[%f, %f, %f, %f], ch.advance >> 6 = %d",
             xpos, ypos, w, h, ch.advance >> 6);

        // 当前字符的顶点坐标
        GLfloat VERTICES[] = {
                xpos + w, ypos,
                xpos + w, ypos + h,
                xpos, ypos,
                xpos, ypos + h
        };

        /**
         * size 几个数字表示一个点,显示是两个数字表示一个点
         * normalized 是否需要归一化,不用,这里已经归一化了
         * stride 步长,连续顶点之间的间隔,如果顶点直接是连续的,也可填0
         */
        // 启用顶点数据
        glEnableVertexAttribArray(positionHandle);
        glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 0, VERTICES);

        // 在方块上绘制字形纹理
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, ch.textureID);
        glUniform1i(textureSampler, 0);
        // 绘制方块
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        // 更新位置到下一个字形的原点,注意单位是1/64像素
        x += (ch.advance >> 6) * scale; //(2^6 = 64)
    }
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);

    // 禁用顶点
    glDisableVertexAttribArray(positionHandle);
    glDisableVertexAttribArray(textureHandle);

}

运行结果:

遇到的问题

  • 测试字体库如何获取

每台个人电脑都自带着很多的字体库,比如搜索.ttf结尾的文件就能搜索到很多,拷贝到手机中进行加载使用测试即可。

  • 使用了Unicode编码依然无法渲染中文

测试的时候发现使用RenderUnicodeText渲染中文有时候还是可能会没有效果,这时候需要确认FT_New_Face加载的这个字体库是否支持中文,如果字体库本身就是不支持中文的话, 那么自然是无法渲染的。

系列教程源码

github.com/feiflyer/ND…

后续demo如果有完善可能会更新。

Opengl ES系列入门介绍

Opengl ES之EGL环境搭建
Opengl ES之着色器
Opengl ES之三角形绘制
Opengl ES之四边形绘制
Opengl ES之纹理贴图
Opengl ES之VBO和VAO
Opengl ES之EBO
Opengl ES之FBO
Opengl ES之PBO
Opengl ES之YUV数据渲染
YUV转RGB的一些理论知识
Opengl ES之RGB转NV21
Opengl ES之踩坑记
Opengl ES之矩阵变换(上)
Opengl ES之矩阵变换(下)
Opengl ES之水印贴图

关注我,一起进步,人生不止coding!!!