1 前言

先来个灵魂拷问:为什么要研究OpenGL渲染文本? 用Android的canvas,不是更香吗?!

这就看应用场景了,一个纯粹的UI界面,确实不需要用到OpenGL,但是,复杂一些的,例如弹幕,用OpenGL,效果就会好很多。

那么Canvas和OpenGL有什么区别?
Canvas是2D图形的API,如果不开启硬件加速,则使用CPU绘制(底层通过skia引擎,纯软件),如果开启硬件加速,则使用GPU绘制(内部通过OpenGLRender把Canvas的工作交给GPU,硬件绘制)。

OpenGL是3D图形的API,默认走GPU绘制,即硬件实现。

所以区别就出来了:手机型号或android版本都可能影响是否支持硬件加速,所以Canvas不能保证都能通过GPU绘制,也就不能保证稳定的帧率,通常可能只能做到30fps。
而OpenGL,直接使用硬件GPU,帧率可以提高到和手机的VSYNC一样,例如60fps!

另外,OpenGL支持3D,可以实现更酷炫的效果。

所以本文讨论的原因有了,真不是瞎折腾,而是事出有因。现在从基础开始,我们怎么把文字给渲染出来。

我们先无脑想一下,渲染文字,需要做什么?

OpenGL基本都离不开纹理,所以其实可以把文字,做成纹理,然后像贴图片一样,贴到屏幕上,不就可以了

例如,我们是一个英文环境,那非常简单,所用的字符,不过就是ASCII码,总共只有256个。

把上面的图,作为一张纹理,绘制哪个文字,就从纹理的哪个坐标截取。通过启用混合,让背景保持透明,最终就能渲染一个字符串到屏幕上。

思路这样没错,但是会有一些问题。
(1) 文字分辨率如何保持?即,一张图如何适配到不同的分辨率屏幕上?
(2)文字颜色如何修改?
(3) 文字如果更多,例如汉字,怎么办?

我们来思考一下每个问题的思路。
(1) 文字分辨率如何保持?
不用上面的一张图表示所有文字,而是每个文字都有一个图。然后,根据代码设置的字号大小,再动态生成每个文字对应分辨率的纹理。有什么第三方库能做到吗?有,那就是FreeType库。后文将会重点展开。

(2)文字颜色如何修改?
加载文字纹理,只提取灰度值(即显示或不显示),颜色在渲染时动态配置。
直接上片段着色器的代码说明。

out vec4 color;
uniform sampler2D text;
uniform vec3 textColor;
void main()
{vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);color = vec4(textColor, 1.0) * sampled;
};

如上,text是文字的纹理。
sampled只提取了纹理的一个灰度值。(用r存灰度值)
textColor是代码配置的文字颜色,color是着色器的输出,即文字的最终效果,颜色来自textColor,透明度来自纹理。

(3) 文字如果更多,例如汉字,怎么办?
把常用的文字提前生成好纹理,绘制时直接使用。 不常用的文字,根据需要动态生成,然后使用。

2 字体文件与解析库FreeType介绍

2.1 字体文件

字体格式类型主要有几个大分类:TrueTypeOpenTypeWOFFSVG。其中最出名的是前两个。下面简单介绍一下。

2.1.1 TrueType

文件后缀是.ttf。微软和苹果联合推出,所以也是Windows和Mac系统最常用的字体格式。
TrueType字体中的字符(或字形)轮廓由直线和二次贝塞尔曲线(bézier)片段构成。所以它和文字大小没关系,放大放小,都可以保证文字的清晰度,锯齿?不存在的。

2.1.2 OpenType

文件后缀是.otf。微软和Adobe联合推出。它也是一种轮廓字体,比TrueType更为强大。特别是体现在跨平台上。

从OpenType文件结构来说,确切地讲它是TrueType 格式的扩展延伸,它在继承了TrueType格式的基础上,增加了对PostScript字型数据(主要应用在打印机)的支持,所以OpenType的字型数据,既可以采用TrueType的字型描述方式,也可以采用PostScript的字型描述方式,这完全由字体厂商来选择决定。从文件结构的角度来讲OpenType或许并不是一种真正新的字体格式,但是该字体格式所增加的排版特性却从功能上为用户开辟了新的用字方式。

2.2 字体解析库FreeType

FreeType是一个开源的字体解析库,非常的通用,windows, ios,android等操作系统,都或多或少用到了这个库。支持各种字体格式,包括上面提的TTF或OTF。想看官网或下载源码,点这里。

FreeType加载一个字体库,很简单。

    if (FT_Init_FreeType(&ft)) {LOGCATE("ERROR::FREETYPE: Could not init FreeType Library");return false;}// find path to fontsstd::string font_name = ASSETS_DIR + "/fonts/Antonio-Bold.ttf";// load fonts as faceFT_Face face;if (FT_New_Face(ft, font_name.c_str(), 0, &face)) {LOGCATE("ERROR::FREETYPE: Failed to load fonts");return false;}

FT_New_Face得到FT_Face。这是一个比较重要的结构体,加载每个文字就通过这个face。

Face加载完成之后,我们需要定义字体大小,这表示着我们要从字体面中生成多大的字形:
例如:

FT_Set_Pixel_Sizes(face, 0, 96);

第二个和第三个参数,代表宽和高。如果将宽度值设为0,表示我们要从字体面通过给定的高度中动态计算出字形的宽度。

3 文字渲染

文字渲染的主要工作:
(1) 用FreeType,提前生成常用文字的bitmap,并作为纹理图片,上传到GPU(1000+个小图片,GPU完全可以承受,不用害怕)
(2) 绘制某个文字时,检查时否已经有纹理数据,没有的话,做(1)的工作。(不再常用文字范围内的文字,就会遇到)
(3) 计算某个文字的顶点坐标
(4) 开始绘制该文字
(5) 重复2~3~4,直到一段文字绘制完成

接下来具体展开讨论。

首先,一个小目标是把文字,生成一个小小的bitmap,作为纹理。当然了,每个文字的宽度是不一样的,这也很好理解,例如一个小点点.和一个字母A,占用的宽度空间,不应该一样。

另外,除了bitmap,freetype还会给出文字的一些数学参数。
来了解一下,一个文字都包含哪些参数:

上面是文字的一些数学参数。其中水平线Baseline最重要(即上面的水平箭头)。每个文字渲染时,应该基于基准线摆放才好看。

下面是一些参数的详细信息:

属性 获取方式 描述
width face->glyph->bitmap.width 位图宽度(像素)
height face->glyph->bitmap.rows 位图高度(像素)
bearingX face->glyph->bitmap_left 水平距离,即位图相对于原点的水平位置(像素)
bearingY face->glyph->bitmap_top 垂直距离,即位图相对于基准线的垂直位置(像素)
advance face->glyph->advance.x 水平预留值,即原点到下一个字形原点的水平距离(单位:1/64像素)

这些参数,在计算文字的顶点坐标时,将会使用到。

好了,激动人心的时刻来了,看一下怎么样加载字体,这里给出一个完整的加载字体的函数。

int TextSample::makeTextAsGLTexture(const wchar_t *text, int size) {// find path to fontsstd::string font_name = ASSETS_DIR + "/fonts/chinese_lvshu.ttf";// load fonts as faceFT_Face face;if (FT_New_Face(ft, font_name.c_str(), 0, &face)) {LOGCATE("ERROR::FREETYPE: Failed to load fonts");return false;}// Set size to load glyphs asFT_Set_Pixel_Sizes(face, 0, 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("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;//This reference will make accessing the bitmap easierFT_Bitmap &bitmap = bitmap_glyph->bitmap;// Generate textureGLuint 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);LOGCATE("initFreeType textureId %d, text[i]=%d [w,h,buffer]=[%d, %d, %p], advance.x=%ld",texture, text[i], bitmap.width, bitmap.rows, bitmap.buffer,glyph->advance.x / MAX_SHORT_VALUE);// Set texture optionsglTexParameteri(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 useCharacter character = {texture,glm::ivec2(bitmap.width, bitmap.rows),glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),static_cast<GLuint>((glyph->advance.x / MAX_SHORT_VALUE) << 6)};LOGCATE("initFreeType, add to slot[%d], size (%d,%d), bearing(%d, %d)", text[i],bitmap.width, bitmap.rows, face->glyph->bitmap_left, face->glyph->bitmap_top);mCharacters.insert(std::pair<GLint, Character>(text[i], character));}glBindTexture(GL_TEXTURE_2D, 0);FT_Done_Face(face);return 0;
}

函数参数 wchar_t *text是文字数组。注意,如果是纯英文字体,则不需要用wchar_t,只要char就行了。wchar_t占用两字节,中文字符是Unicode编码,需要两个字节。

再来看一下makeTextAsGLTexture函数都做了什么

(1) FT_Load_Glyph: 加载文字数组的某个文字;
(2) FT_Get_Glyph(face->glyph, &glyph): 从face结构体中,提取glyph
(3) FT_Glyph_To_Bitmap : 生成文字的bitmap
(4) glGenTextures & glTexImage2D: 把bitmap生成纹理,上传到GPU
(5) mCharacters.insert: 把纹理id,文字的数学参数一起存储,方便渲染时查询并使用。

好了,我们来看一下函数怎么调用:

static const wchar_t CHINESE_COMMON[] = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz12367890的一是了我不人在他有这个上们来到时大地为子中你说生国年着就那和";makeTextAsGLTexture(CHINESE_COMMON, sizeof(CHINESE_COMMON) / sizeof(CHINESE_COMMON[0]) - 1);

CHINESE_COMMON数组定义了常用的中文文字,大概1000多个,因为篇幅原因,我就没全贴了,可以看文章最后,我的源码。当然了,其实中文字体库,也包括了英文字母和数字,毕竟只有几十个,支持一下很简单,所以把英文字母也加上了。这样就不用即加载中文字体,又加载英文字体了。

makeTextAsGLTexture在初始化时调用一次即可。

接下来,给一个渲染函数RenderTextChinese,每次onDraw时都会调用。

void TextSample::RenderTextChinese(Shader *shader, const wchar_t *text, int textLen, GLfloat x,GLfloat y, GLfloat scale,glm::vec3 color, glm::vec2 viewport) {// 激活合适的渲染状态shader->setVec3("textColor", color);glBindVertexArray(VAO);checkGLError("RenderTextChinese");x *= viewport.x;y *= viewport.y;for (int i = 0; i < textLen; ++i) {Character ch;getCharacter(text[i], ch);LOGCATD("RenderTextChinese, slot[%d], textureId %d", text[i], ch.TextureID);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;LOGCATD("RenderTextChinese [xpos,ypos,w,h]=[%f, %f, %f, %f]", xpos, ypos, w, h);// 当前字符的VBOGLfloat 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);checkGLError("RenderTextChinese 2");// 更新当前字符的VBOglBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);checkGLError("RenderTextChinese 3");glBindBuffer(GL_ARRAY_BUFFER, 0);// 绘制方块glDrawArrays(GL_TRIANGLES, 0, 6);checkGLError("RenderTextChinese 4");// 更新位置到下一个字形的原点,注意单位是1/64像素x += (ch.Advance >> 6) * scale; //(2^6 = 64)}glBindVertexArray(0);glBindTexture(GL_TEXTURE_2D, 0);
}

这个函数具体工作:
(1) getCharacter(text[i], ch); 根据文字的Unicode的值,读取上面存的Character结构体,如果读取不到,说明不是常用的文字,getCharacter函数内会再次调用makeTextAsGLTexture,生成这个生僻字的对应的Character并返回。
代码不多,直接明示:

void TextSample::getCharacter(const wchar_t oneText, Character &ch) {if (mCharacters.find(oneText) != mCharacters.end()) {ch = mCharacters[oneText];} else {LOGCATD("getCharacter, make a new text");const wchar_t temp[] = {oneText};makeTextAsGLTexture(temp, 1);ch = mCharacters[oneText];}
}

(2) GLfloat vertices[6][4] 生成文字对应的顶点坐标,使用的是归一化坐标,即【-1.0f ~ 1.0f】。一个文字四边形,对应2个三角形,所以需要6个顶点。因为是3D场景,所以每个坐标是4个值(x, y, z, w)。
(3) glBindTexture 绑定文字对应的纹理
(4) glBindBuffer 绑定顶点
(5) glDrawArrays 绘制一个文字
(6) for循环遍历步骤1~5,绘制完一个文字数组。

来看一下函数的调用方式:

static const wchar_t CHINESE_TEST[] = L"Love小爱心HAHA";glm::vec2 viewport(screenW, screenH);RenderTextChinese(m_pShader, CHINESE_TEST,sizeof(CHINESE_TEST) / sizeof(CHINESE_TEST[0]) - 1, -0.9f, -0.1f, 1.0f,glm::vec3(0.5f, 0.8f, 0.2f), viewport);

CHINESE_TEST是输入的文字数组。

为什么第三个参数,sizeof(CHINESE_TEST) / sizeof(CHINESE_TEST[0]) - 1 要减去1?因为还有一个换行符号\,这个不需要渲染。

至此,关键的工作已经说明完成,但相信有很多细节你还想了解,请直接看我的源码哈:
OpenGLESDemo

效果图

参考

Learn OpenGL Text-Rendering

Android OpenGL ES 渲染文本相关推荐

  1. android OpenGL ES实现渲染到透明的纹理 render to transparent texture

    PC上OpenGL渲染到纹理,很容易得到透明背景,但是在android上OpenGL ES渲染出来是黑色背景,对于这个问题,想了两个解决办法. 1> 让android的OpenGL ES环境支持 ...

  2. Android OpenGL ES视频渲染(一)GLSurfaceView

    相关文章:Android OpenGL ES视频渲染(二)EGL+OpenGL Android中视频渲染有几种方式,之前的文章使用的是nativewindow(包括softwareRender).今天 ...

  3. Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  4. Android OpenGL ES 开发教程(20):颜色Color

    OpenGL ES 支持的颜色格式为RGBA模式(红,绿,蓝,透明度).颜色的定义通常使用Hex格式0xFF00FF 或十进制格式(255,0,255), 在OpenGL 中却是使用0-1之间的浮点数 ...

  5. OpenGl文章 Android OpenGL ES 简明开发教程

    Android OpenGL ES 简明开发教程 分类:android学习笔记2011-12-14 15:04375人阅读评论(0)收藏举报 ApiDemos 的Graphics示例中含有OpenGL ...

  6. Android OpenGL ES 从入门到精通系统性学习教程

    1 为什么要写这个教程 目前这个 OpenGL ES 极简教程的更新暂时告一段落,在此之前,很荣幸获得了阮一峰老师的推荐. 因为在工作中频繁使用 OpenGL ES 做一些特效.滤镜之类的效果,加上平 ...

  7. Android OpenGL ES 画出三棱锥

    如今VR这么火,感觉有必要学学OpenGL.什么是OpenGL ES ,OpenGL ES (OpenGL for Embedded System ) 为适用于嵌入式系统的一个免费二维和三维图形库.O ...

  8. Android OpenGL ES 基础原理

    由于5G的发展,现在音视频越来越流行,我们的生活已经完全被抖音.视频号.B站等视频应用所包围.从这一点也能看到音视频的重要性. 而作为一名Android开发者,是时候来了解一下关于Android方面渲 ...

  9. Android OpenGL ES 学习(二) -- 图形渲染管线和GLSL

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

最新文章

  1. 使用Docker快速部署禅道V11.6版本
  2. ASP.NET中密码保护,MD5和SHA1算法的使用
  3. 【Flink】FLink SQL TableException: Table sink doesn‘t support consuming update changes which is
  4. (转) Dockerfile 中的 COPY 与 ADD 命令 1
  5. python三维数组表示方法_Python操作多维数组和矩阵
  6. [大妈吐糟] 虾米音乐的系列猜想
  7. Axis2 生成客户端
  8. 电压转换速率(Slew Rate,SR)
  9. 压缩包加密破解常见方法总结 CTF中Misc必备
  10. 作为一名优秀的程序员,如何选购适合自己的显示器
  11. 一群在全球顶会崭露头角的阿里新生代白帽:能查漏洞还会焊接
  12. 如何克服自己的懒惰-第二弹
  13. 快速开发小程序——案例
  14. 5款小巧有趣的微信小程序,个个让你心花怒放!
  15. 最全工业以太网通讯协议
  16. anaconda下使用python怎样实现图像增强_如何用anaconda进行python开发
  17. 数学一年级应用题_2019年小学一年级数学应用题汇总
  18. 第八章 云计算原理与技术
  19. 游戏感:虚拟感觉的游戏设计师指南——第十五章 超级马里奥64
  20. 一维卷积神经网络原理,一维卷积神经网络应用

热门文章

  1. 新手小白怎么做shopee虾皮跨境?记住这三点不会被割“韭菜”
  2. 文件系统挂载选项journal
  3. 一对老耗子,每个月都生一对小耗子。小耗子长3个月,第四个开始变成老耗子开始生! 假如都不死,那么请问24个月后有多少只耗子?...
  4. 移动端tab切换时下划线的滑动效果
  5. 有趣且鲜为人知的 Python 特性,火了!
  6. Exact Audio Copy
  7. 快速打开浏览倾斜摄影数据教程
  8. Qt实用技巧:使用QMediaPlayer和Windows自带组件播放swf、rmvb、mpg、mp4等视频文件
  9. html游戏导出存档,Savedatafiler使用教程 Savedatafiler导出cia存档
  10. 用科学计算机打游戏,10个惊人的游戏,竟然有助于科学研究,你玩过几个?