在音视频或 OpenGL 开发中,文字渲染是一个高频使用的功能,好比制做一些酷炫的字幕、为视频添加水印、设置特殊字体等等。java
实际上 OpenGL 并无定义渲染文字的方式,因此咱们最能想到的办法是:将带有文字的图像上传到纹理,而后进行纹理贴图。android
本文分别介绍下在应用层和 C++ 层经常使用的文字渲染方式。c++
在应用层实现文字渲染主要是利用 Canvas 将文本绘制成 Bitmap ,而后生成一张小图,而后在渲染的时候进行贴图。git
在实际的生产环境中,通常会将这张小图转换成灰度图,减小没必要要的数据拷贝和内存占用,而后在渲染的时候能够为灰度图上色,做为字体的颜色。github
// 建立一个 bitmap
Bitmap bitmap = Bitmap.createBitmap(width, hight, Bitmap.Config.ARGB_8888);
// 初始化画布绘制的图像到 bitmap 上
Canvas canvas = new Canvas(bitmap);
// 创建画笔
Paint paint = new Paint();
// 获取更清晰的图像采样,防抖动
paint.setDither(true);
paint.setFilterBitmap(true);
// 绘制文字到 bitmap
canvas.drawText text, x, y,paint);
复制代码
而后生成纹理,将 bitmap 上传到纹理。canvas
int[] textureIds = new int[1];
//建立纹理
GLES20.glGenTextures(1, textureIds, 0);
mTexId = textureIds[0];
//绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexId);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
ByteBuffer bitmapBuffer = ByteBuffer.allocate(bitmap.getHeight() * bitmap.getWidth() * 4);//RGBA
bitmap.copyPixelsToBuffer(bitmapBuffer);
bitmapBuffer.flip();
//设置内存大小绑定内存地址
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mWatermarkBitmap.getWidth(), mWatermarkBitmap.getHeight(),
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, bitmapBuffer);
//解绑纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
复制代码
最后将带有文字的纹理映射到对应的位置(纹理贴图)。api
FreeType 是一个基于 C 语言实现的用于文字渲染的开源库,它小巧、高效、高度可定制,主要用于加载字体并将其渲染到位图,支持多种字体的相关操做。缓存
FreeType 也是一个很是受欢迎的跨平台字体库,支持 Android、 iOS、 Linux 等操做系统。TrueType 字体不采用像素或其余不可缩放的方式来定义,而是一些经过数学公式(曲线的组合)。这些字形,相似于矢量图像,能够根据你须要的字体大小来生成像素图像。微信
FreeType 官网地址:markdown
https://www.freetype.org/
复制代码
本小节主要介绍使用 NDK 编译 Android 平台使用的 FreeType 库。首先在官网上下载最新版的 FreeType 源码,而后新建一个 jni 文件夹,将源码放到 jni 文件夹里,目录结构以下所示:
新建构建文件 Android.mk 和 Application.mk。
Android.mk 参考 Google 的构建脚本:
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
./src/autofit/autofit.c \
./src/base/ftbase.c \
./src/base/ftbbox.c \
./src/base/ftbdf.c \
./src/base/ftbitmap.c \
./src/base/ftcid.c \
./src/base/ftdebug.c \
./src/base/ftfstype.c \
./src/base/ftgasp.c \
./src/base/ftglyph.c \
./src/base/ftgxval.c \
./src/base/ftinit.c \
./src/base/ftlcdfil.c \
./src/base/ftmm.c \
./src/base/ftotval.c \
./src/base/ftpatent.c \
./src/base/ftpfr.c \
./src/base/ftstroke.c \
./src/base/ftsynth.c \
./src/base/ftsystem.c \
./src/base/fttype1.c \
./src/base/ftwinfnt.c \
./src/bdf/bdf.c \
./src/bzip2/ftbzip2.c \
./src/cache/ftcache.c \
./src/cff/cff.c \
./src/cid/type1cid.c \
./src/gzip/ftgzip.c \
./src/lzw/ftlzw.c \
./src/pcf/pcf.c \
./src/pfr/pfr.c \
./src/psaux/psaux.c \
./src/pshinter/pshinter.c \
./src/psnames/psmodule.c \
./src/raster/raster.c \
./src/sfnt/sfnt.c \
./src/smooth/smooth.c \
./src/tools/apinames.c \
./src/truetype/truetype.c \
./src/type1/type1.c \
./src/type42/type42.c \
./src/winfonts/winfnt.c
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_CFLAGS += -W -Wall
LOCAL_CFLAGS += -fPIC -DPIC
LOCAL_CFLAGS += "-DDARWIN_NO_CARBON"
LOCAL_CFLAGS += "-DFT2_BUILD_LIBRARY"
LOCAL_CFLAGS += -O2
LOCAL_MODULE:= freetype
include $(BUILD_STATIC_LIBRARY)
#https://android.googlesource.com/platform/external/freetype/+/android-6.0.1_r28/Android.mk
复制代码
Application.mk:
APP_OPTIM := release
APP_CPPFLAGS := -std=c++14 -frtti
NDK_TOOLCHAIN_VERSION := clang
APP_PLATFORM := android-28
APP_STL := c++_static
APP_ABI := arm64-v8a,armeabi-v7a
复制代码
最后 jni 目录下命令行执行 ndk-build 指令便可,若是不想编译,也能够直接到下面项目取现成的静态库:
https://github.com/githubhaohao/NDK_OpenGLES_3_0
复制代码
引入头文件:
#include "ft2build.h"
#include <freetype/ftglyph.h>
复制代码
而后要加载一个字体,咱们须要作的是初始化 FreeType 而且将这个字体加载为 FreeType 称之为面 Face 的东西。这里我在 Windows 下找了个字体文件 Antonio-Regular.ttf ,放到 sdcard 下面供 FreeType 加载。
FT_Library ft;
if (FT_Init_FreeType(&ft))
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYPE: Could not init FreeType Library");
FT_Face face;
if (FT_New_Face(ft, "/sdcard/fonts/Antonio-Regular.ttf", 0, &face))
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYPE: Failed to load font");
FT_Set_Pixel_Sizes(face, 0, 96);
复制代码
代码片断中,FT_Set_Pixel_Sizes 用于设置文字的大小,此函数设置了字体面的宽度和高度,将宽度值设为0表示咱们要从字体面经过给出的高度中动态计算出字形的宽度。
一个字体面中 Face 包含了全部字形的集合,咱们能够经过调用 FT_Load_Char 函数来激活当前要表示的字形。这里咱们选在加载字母字形 'A':
if (FT_Load_Char(face, 'A', FT_LOAD_RENDER))
std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
复制代码
经过将 FT_LOAD_RENDER 设为一个加载标识,咱们告诉 FreeType 去建立一个 8 位的灰度位图,咱们能够经过face->glyph->bitmap 来取得这个位图。
使用 FreeType 加载的字形位图并不像咱们使用位图字体那样持有相同的尺寸大小。使用FreeType生产的字形位图的大小是刚好能包含这个字形的尺寸。例如生产用于表示 '.' 的位图的尺寸要比表示 'A' 的小得多。
所以,FreeType在加载字形的时候还生产了几个度量值来描述生成的字形位图的大小和位置。下图展现了 FreeType 的全部度量值的涵义。
那么多属性其实不用刻意取记住,这里只是做为概念性了解。最后,使用完 FreeType 记得释放相关资源:
FT_Done_Face(face);
FT_Done_FreeType(ft);
复制代码
按照前面的思路,使用 FreeType 加载字形的位图而后生成纹理,而后进行纹理贴图。
然而每次渲染的时候都去从新加载位图显然不是高效的,咱们应该将这些生成的数据储存在应用程序中,在渲染过程当中再去取,重复利用。
方便起见,咱们须要定义一个用来储存这些属性的结构体,并建立一个字符表来存储这些字形属性。
struct Character {
GLuint textureID; // ID handle of the glyph texture
glm::ivec2 size; // Size of glyph
glm::ivec2 bearing; // Offset from baseline to left/top of glyph
GLuint advance; // Horizontal offset to advance to next glyph
};
std::map<GLint, Character> m_Characters;
复制代码
简单起见,咱们只生成表示 128 个 ASCII 字符的字符表,并为每个字符储存纹理和一些度量值。这样,全部须要的字符就被存下来备用了。
void TextRenderSample::LoadFacesByASCII() {
// FreeType
FT_Library ft;
// All functions return a value different than 0 whenever an error occurred
if (FT_Init_FreeType(&ft))
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYPE: Could not init FreeType Library");
// Load font as face
FT_Face face;
if (FT_New_Face(ft, "/sdcard/fonts/Antonio-Regular.ttf", 0, &face))
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYPE: Failed to load font");
// Set size to load glyphs as
FT_Set_Pixel_Sizes(face, 0, 96);
// Disable byte-alignment restriction
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))
{
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYTPE: Failed to load Glyph");
continue;
}
// Generate texture
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));
}
glBindTexture(GL_TEXTURE_2D, 0);
// Destroy FreeType once we're finished
FT_Done_Face(face);
FT_Done_FreeType(ft);
}
复制代码
针对 OpenGL ES 灰度图要使用的纹理格式是 GL_LUMINANCE 而不是 GL_RED 。
OpenGL 纹理对应的图像默认要求 4 字节对齐,这里须要设置为 1 ,确保宽度不是 4 倍数的位图(灰度图)可以正常渲染。
渲染文字使用的 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 主要是针对屏幕坐标进行归一化:
void TextRenderSample::RenderText(std::string text, 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();
// 对文本中的全部字符迭代
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;
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);
}
复制代码
使用 RenderText 渲染 2 个文本:
// (x,y)为屏幕坐标系的位置,即原点位于屏幕中心,x(-1.0,1.0), y(-1.0,1.0)
RenderText("My WeChat ID is Byte-Flow.", -0.9f, 0.2f, 1.0f, glm::vec3(0.8, 0.1f, 0.1f), viewport);
RenderText("Welcome to add my WeChat.", -0.9f, 0.0f, 2.0f, glm::vec3(0.2, 0.4f, 0.7f), viewport);
复制代码
完整实现代码见项目: github.com/githubhaoha…
文本渲染效果:
learnopengl.com/In-Practice… android.googlesource.com/platform/ex…
技术交流/获取视频教程能够添加个人微信:Byte-Flow