OpenGL ES 文字渲染进阶–渲染中文字体

旧文 OpenGL ES 文字渲染方式有几种? 一文中分别介绍了 OpenGL 利用 Canvas 和 FreeType 绘制文字的方法。

无论采用哪种方式进行渲染,本质上原理都是纹理贴图:将带有文字的图像上传到纹理,然后进行贴图。

OpenGL ES 文字渲染进阶--渲染中文字体

渲染中文字体

利用 Canvas 绘制中文字体和绘制其他字体在操作方式上没有区别,但是使用 FreeType 绘制中文字体,在编码方式、加载方式以及字体属性上面会有一些坑要踩,这里本人已经踩过,将在本文中分享给各位读者大人。

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

TrueType 字体不采用像素或其他不可缩放的方式来定义,而是一些通过数学公式(曲线的组合)。这些字形,类似于矢量图像,可以根据你需要的字体大小来生成像素图像。

FreeType 官网地址:

https://www.freetype.org/

关于 FreeType 开源库多个平台的编译方法,同样请参考旧文 OpenGL ES 文字渲染方式有几种? ,这里不再重复讲述。

使用 FreeType 渲染中文和英文字符在流程上基本一致,都是根据字符的编码值来加载位图,然后上传纹理。

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

FT_Select_Charmap(face, ft_encoding_unicode);

另外,中文字符串需要采用宽字符 wchar_t

FreeType 加载中文字符位图需要,先根据 Unicode 编码值查询位图的索引,然后根据索引获取到 FreeType 的 Glyph 对象,最后再将 FT_Glyph 转换为 FT_BitmapGlyph 获取到字体的位图。

加载中文字符串对应位图的代码如下;

void TextRenderSample::LoadFacesByUnicode(const wchar_t* text, int size) {
    // FreeType
    FT_Library ft;
    // All functions return a value different than 0 whenever an error occurred
    if (FT_Init_FreeType(&ft))
        LOGCATE("TextRenderSample::LoadFacesByUnicode FREETYPE: Could not init FreeType Library");

    // Load font as face
    FT_Face face;
    std::string path(DEFAULT_OGL_ASSETS_DIR);
    if (FT_New_Face(ft, (path + "/fonts/msyh.ttc").c_str(), 0, &face))
        LOGCATE("TextRenderSample::LoadFacesByUnicode FREETYPE: Failed to load font");

    // Set size to load glyphs as
    FT_Set_Pixel_Sizes(face, 96, 96);
    FT_Select_Charmap(face, ft_encoding_unicode);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    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))
        {
            LOGCATE("TextRenderSample::LoadFacesByUnicode FREETYTPE: Failed to load Glyph");
            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;
        FT_Bitmap& bitmap = bitmap_glyph->bitmap;

        // 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));

    }
    glBindTexture(GL_TEXTURE_2D, 0);
    // Destroy FreeType once we're finished
    FT_Done_Face(face);
    FT_Done_FreeType(ft);
}

代码中 glyph->advance.x / MAX_SHORT_VALUE 相当于向右移 16 位,是从 FreeType 官方文档中得出来的结论。

值得反复强调的地方,针对 OpenGL ES 灰度图要使用的纹理格式是 GL_LUMINANCE 而不是 GL_RED 。

OpenGL 纹理对应的图像默认要求 4 字节对齐,这里需要设置为 1 ,确保宽度不是 4 倍数的位图(灰度图)能够正常渲染。

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

渲染文字使用的 shader :

//vertex shader
#version 300 es
layout(location = 0) in vec4 a_position;// <vec2 pos, vec2 tex>
uniform mat4 u_MVPMatrix;
out vec2 v_texCoord;
void main()
{
    gl_Position = u_MVPMatrix * vec4(a_position.xy, 0.0, 1.0);;
    v_texCoord = a_position.zw;
}

//fragment shader
#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_textTexture;
uniform vec3 u_textColor;

void main()
{
    vec4 color = vec4(1.0, 1.0, 1.0, texture(s_textTexture, v_texCoord).r);
    outColor = vec4(u_textColor, 1.0) * color;
}

片段着色器有两个 uniform 变量:一个是单颜色通道的字形位图纹理,另一个是文字的颜色,我们可以同调整它来改变最终输出的字体颜色。

开启混合,去掉文字背景。

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

生成一个 VAO 和一个 VBO ,用于管理的存储顶点、纹理坐标数据,GL_DYNAMIC_DRAW 表示我们后面要使用 glBufferSubData 不断刷新 VBO 的缓存。


glGenVertexArrays(1, &m_VaoId);
glGenBuffers(1, &m_VboId);

glBindVertexArray(m_VaoId);
glBindBuffer(GL_ARRAY_BUFFER, m_VboId);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);
glBindVertexArray(GL_NONE);

每个 2D 方块需要 6 个顶点,每个顶点又是由一个 4 维向量(一个纹理坐标和一个顶点坐标)组成,因此我们将VBO 的内存分配为 6*4 个 float 的大小。

渲染中文字体的函数如下,其中传入 viewport 主要是针对屏幕坐标进行归一化:

vvoid TextRenderSample::RenderText(const wchar_t *text, int textLen, GLfloat x, GLfloat y, GLfloat scale,
                                  glm::vec3 color, glm::vec2 viewport) {
    // 激活合适的渲染状态
    glUseProgram(m_ProgramObj);
    glUniform3f(glGetUniformLocation(m_ProgramObj, "u_textColor"), color.x, color.y, color.z);
    glBindVertexArray(m_VaoId);
    GO_CHECK_GL_ERROR();
    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;

        LOGCATE("TextRenderSample::RenderText [xpos,ypos,w,h]=[%f, %f, %f, %f]", xpos, ypos, w, h);

        // 当前字符的VBO
        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 }
        };

        // 在方块上绘制字形纹理
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, ch.textureID);
        glUniform1i(m_SamplerLoc, 0);
        GO_CHECK_GL_ERROR();
        // 更新当前字符的VBO
        glBindBuffer(GL_ARRAY_BUFFER, m_VboId);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
        GO_CHECK_GL_ERROR();
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        // 绘制方块
        glDrawArrays(GL_TRIANGLES, 0, 6);
        GO_CHECK_GL_ERROR();
        // 更新位置到下一个字形的原点,注意单位是1/64像素
        x += (ch.advance >> 6) * scale; //(2^6 = 64)
    }
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);
}

加载和渲染中文字体:

static const wchar_t BYTE_FLOW[] = L"OpenES 渲染中文字体";

// 加载中文字体
LoadFacesByUnicode(BYTE_FLOW, sizeof(BYTE_FLOW)/sizeof(BYTE_FLOW[0]) - 1);

// (x,y)为屏幕坐标系的位置,即原点位于屏幕中心,x(-1.0,1.0), y(-1.0,1.0)
// 渲染中文字体
RenderText(BYTE_FLOW, sizeof(BYTE_FLOW)/sizeof(BYTE_FLOW[0]) - 1, -0.9f, -0.2f, 1.0f, glm::vec3(0.7, 0.4f, 0.2f), viewport);

完整实现代码见项目:

https://github.com/githubhaohao/NDK_OpenGLES_3_0

完整实现代码见项目:
https://github.com/githubhaohao/NDK_OpenGLES_3_0

进技术交流群,添加我的微信:Byte-Flow

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/18110.html

(0)

相关推荐

  • OpenGL ES 3D 模型的加载和渲染

    OpenGL ES 3D 模型加载和渲染 3D 模型渲染 上一节简单介绍了常用的 3D 模型文件 Obj 的数据结构和模型加载库 Assimp 的编译,本节主要介绍如何使用 Ass…

    2022年4月3日
  • Opengl ES之矩阵变换(下)

    在上一节 《Opengl ES之矩阵变换(上)》 中,我们通过矩阵变换实现一个一些形变的效果。 如果细心的童鞋们可能会发现,我们的运行结果渲染的图片宽高明显是有些变形了,特别是在手…

    2023年2月25日
  • Opengl ES之转场动画

    转场 什么是转场效果?一般来说,就是两个视频画面之间的过渡衔接效果。在opengl中,图片的转场,其实就是两个纹理的过渡切换,一般会有两个纹理作为输入,一个是逐渐消失的纹理,一个是…

    2023年7月3日
  • Opengl ES之水印贴图

    水印贴图又称画中画,这种功能在Opengl中是如何实现的呢?我们可以简单地理解成两张纹理的叠加,一个纹理作为背景,另外一个纹理通过调整顶点坐标作为一个小的前景。 说到水印贴图的实现…

    2023年2月26日
  • 什么是裁剪测试?在Opengl中如何使用裁剪测试

    什么是裁剪测试 剪裁测试用于限制绘制区域,在 OpenGL 中启用裁剪测试可以在屏幕或者帧缓冲上指定一个矩形区域,然后在该矩形区域内绘制,只有在该区域内的片元才有机会最终进入帧缓冲…

    2023年7月3日
  • OpenGL ES渲染播放视频

    前面文章主要了解了 OpenGL ES 的基本使用及其坐标系的映射,下面将使用MediaPlayer和 OpenGL ES 来实现基本视频渲染以及视频画面的矫正,主要内容如下: S…

    2023年2月22日

发表回复

登录后才能评论