在音視頻或 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