在音视频或 OpenGL 开发中,文字渲染是一个高频使用的功能,比如制作一些酷炫的字幕、为视频添加水印、设置特殊字体等等。
实际上 OpenGL 并没有定义渲染文字的方式,所以我们最能想到的办法是:将带有文字的图像上传到纹理,然后进行纹理贴图。
本文分别介绍下在应用层和 C++ 层常用的文字渲染方式。
OpenGL ES 文字渲染
在应用层实现文字渲染主要是利用 Canvas 将文本绘制成 Bitmap ,然后生成一张小图,然后在渲染的时候进行贴图。
在实际的生产环境中,一般会将这张小图转换成灰度图,减少不必要的数据拷贝和内存占用,然后在渲染的时候可以为灰度图上色,作为字体的颜色。
// 创建一个 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 上传到纹理。
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);
最后将带有文字的纹理映射到对应的位置(纹理贴图)。
FreeType 是一个基于 C 语言实现的用于文字渲染的开源库,它小巧、高效、高度可定制,主要用于加载字体并将其渲染到位图,支持多种字体的相关操作。
FreeType 也是一个非常受欢迎的跨平台字体库,支持 Android、 iOS、 Linux 等操作系统。
TrueType 字体不采用像素或其他不可缩放的方式来定义,而是一些通过数学公式(曲线的组合)。这些字形,类似于矢量图像,可以根据你需要的字体大小来生成像素图像。
FreeType 官网地址:
https://www.freetype.org/
本小节主要介绍使用 NDK 编译 Android 平台使用的 FreeType 库。
首先在官网上下载最新版的 FreeType 源码,然后新建一个 jni 文件夹,将源码放到 jni 文件夹里,目录结构如下所示:
FreeType 目录结构
新建构建文件 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 的所有度量值的涵义。
glyph.png
那么多属性其实不用刻意取记住,这里只是作为概念性了解。最后,使用完 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 倍数的位图(灰度图)能够正常渲染。
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 主要是针对屏幕坐标进行归一化:
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);
完整实现代码见项目: https://github.com/githubhaohao/NDK_OpenGLES_3_0
文本渲染效果:
文本渲染效果
https://learnopengl.com/In-Practice/Text-Rendering https://android.googlesource.com/platform/external/freetype/+/android-6.0.1_r28/Android.mk
-- END --
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/-9DGwlp6dWAnBsdq1JRbFw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。