前言:
一直在从事相关的行业,忽然想起来是不是要把前面的积累给整理记录下来。也是心血来潮的不知道能够坚持过久(或者说有多少的积累吧。。。。)
先分享关于这个freetype的使用,后面有机会了在分享其他的内容。
PS: 在做这个功能时候,有参考前辈的资料。但是时间有点久,参考那些大佬的博客、文献资料已经记不清了,在这里先说一句抱歉哈。
进入正题:
一、经典文本渲染:位图字体
有个项目上面使用,用于软件版本信息显示。
方案: 请造型同事帮忙制作一张包含显示信息的字库图片,大小:256*256,宽高平均分成10*10个大小的区域。
使用:
绘制时从字库图片截取对应ROI区域的内容,指定好显示位置一起用于界面的显示。
效果:
二、现代文件渲染:FreeType
FreeType是一个能够用于加载字体并将他们渲染到位图以及提供多种字体相关的操作的软件开发库。它是一个非常受欢迎的跨平台字体库,它被用于Mac OS X、Java、PlayStation主机、Linux、Android等平台。FreeType的真正吸引力在于它能够加载TrueType字体。
1、freetyp下载、编译、安装
下载:
wget https://download.savannah.gnu.org/releases/freetype/freetype-2.13.1.tar.gz
tar -xzf freetype-2.13.1.tar.gz
cd freetype-2.13.1编译
./configure --prefix=/usr/local/freetype #可以自己指定目录 $pwn/gen
选项–prefix=/usr/local/freetype,是在安装时将软件安装到/usr/local/freetype目录下
make && make install
//对软件源代码文件进行编译并安装
3、freetype的使用
一、准备字体
FreeType所做的事就是加载TrueType字体并为每一个字形生成位图以及计算几个度量值(Metric)。我们可以提取出它生成的位图作为字形的纹理,并使用这些度量值定位字符的字形。
二、头文件使用
头文件
#include <ft2build.h>
#include FT_FREETYPE_H
三、freetype初始化操作
要加载一个字体,我们只需要初始化FreeType库,并且将这个字体加载为一个FreeType称之为面(Face)
的东西。这里为我们加载一个从Fonts目录中拷贝来的TrueType字体文件arial.ttf。
FT_Library ft;
if (FT_Init_FreeType(&ft))
std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl;
FT_Face face;
if (FT_New_Face(ft, "fonts/arial.ttf", 0, &face))
std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;
这些FreeType函数在出现错误时将返回一个非零的整数值。
FT_Set_Pixel_Sizes(face, 0, 48);
此函数设置了字体面的宽度和高度,将宽度值设为0表示我们要从字体面通过给定的高度中动态计算出字形的宽度。
一个FreeType面中包含了一个字形的集合。我们可以调用FT_Load_Char函数来将其中一个字形设置为激活字形。这里我们选择加载字符字形‘X’:
if (FT_Load_Char(face, 'X', FT_LOAD_RENDER))
std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
通过将FT_LOAD_RENDER设为加载标记之一,我们告诉FreeType去创建一个8位的灰度位图.
在OpenGL中位图表示黑白的数据,而在FreeType中是使用8位的颜色表示位图,所以FreeType的位图可以保存亮度信息。
PS:可以使用freeimage库保存freetype库生成的位图图片。
如下:
unsigned char* buf = face->glyph->bitmap.buffer;
int width = face->glyph->bitmap.width;
int height = face->glyph->bitmap.rows;
int pitch = face->glyph->bitmap.pitch;
cout << "pitch=" << pitch << endl;// 23
FIBITMAP* fibm = FreeImage_ConvertFromRawBits(buf, width, height, pitch, 8, 0, 0, 0);
FreeImage_Save(FIF_BMP, fibm, "Glyph.bmp");
保存后的数字和字母的位图:
拿其中一个来说明:
通过取色,黑色部分是纯黑RGB=(0,0,0),白色部分为纯白RGB=(255,255,255)。
纯白部分是文字部分。
因为face->glyph->bitmap.pitch大于0,所以位图是经垂直翻转,呈倒立的。
使用FreeType加载的每个字形没有相同的大小(不像位图字体那样)。使用FreeType生成的位图的大小恰好能包含这个字符可见区域。例如生成用于表示‘.’的位图的大小要比表示‘X’的小得多。因此,FreeType同样也加载了一些度量值来指定每个字符的大小和位置。下面这张图展示了FreeType对每一个字符字形计算的所有度量值。
四、定义字符的结构体
在需要渲染字符时,我们可以加载一个字符字形,获取它的度量值,并生成一个纹理,但每一帧都这样做会非常没有效率。我们应将这些生成的数据储存在程序的某一个地方,在需要渲染字符的时候再去调用。我们会定义一个非常方便的结构体,并将这些结构体存储在一个map中。
struct Character {
GLuint TextureID; // 字形纹理的ID
glm::ivec2 Size; // 字形大小
glm::ivec2 Bearing; // 从基准线到字形左部/顶部的偏移值
GLuint Advance; // 原点距下一个字形原点的距离
};
std::map<GLchar, Character> Characters;
五、生成字符纹理数据
对于这个教程来说,本着让一切简单的目的,我们只生成ASCII字符集的前128个字符。对每一个字符,我们生成一个纹理并保存相关数据至Character结构体中,之后再添加至Characters这个映射表中。这样子,渲染一个字符所需的所有数据就都被储存下来备用了。
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); //禁用字节对齐限制,设置为1字节对齐
for (GLubyte c = 0; c < 128; c++) {
// 加载字符的字形
if (FT_Load_Char(face, c, FT_LOAD_RENDER)) {
std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
continue;
}
// 生成纹理
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RED,
face->glyph->bitmap.width,
face->glyph->bitmap.rows,
0,
GL_RED,
GL_UNSIGNED_BYTE,
face->glyph->bitmap.buffer
);
// 设置纹理选项
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);
// 储存字符供之后使用
Character character = {
texture,
glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
face->glyph->advance.x
};
Characters.insert(std::pair<GLchar, Character>(c, character));
}// end for loop
在这个for循环中我们遍历了ASCII集中的全部128个字符,并获取它们对应的字符字形。对每一个字符,我们生成了一个纹理,设置了它的选项,并储存了它的度量值。有趣的是我们这里将纹理的参数internalFormat和format都设置为GL_RED。通过字形生成的位图是一个8位灰度图,它的每一个颜色都由一个字节(8bit)来表示。因此我们需要将位图缓冲的每一字节都作为纹理的颜色值。这是通过创建一个特殊的纹理实现的,这个纹理的每一字节都对应着纹理颜色的红色分量(颜色向量的第一个字节)。如果我们使用一个字节来表示纹理的颜色,我们需要注意OpenGL的一个限制:
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
OpenGL要求所有的纹理都是4字节对齐的,即纹理的大小永远是4字节的倍数。通常这并不会出现什么问题,因为大部分纹理的宽度都为4的倍数并/或每像素使用4个字节,但是现在我们每个像素只用了一个字节,它可以是任意的宽度。通过将纹理解压对齐参数
设为1,这样才能确保不会有对齐问题(它可能会造成段错误)。
六、释放freetype资源
FT_Done_Face(face);
FT_Done_FreeType(ft);
4、freetype实践
Q: 上面第五点使用ASCII字符集生成的对应的纹理数据,但是如果是汉字时该怎么处理?
PS: 多字节字符
C语言中基本数据类型之一的char类型(字符),一个英文字符占用一个字节,char * 表示的字符串中也是一个英文字符占用一个字节,包括结尾符'\0'(空字符)也只占用一个字节。
但其它语言的字符就不能只用一个字节表示了,例如:汉字,仅常用汉字就有3500多个,加上生僻汉字超过10万多个。而且汉字复杂,一个汉字可能2个字节、3个字节等。若一个字符占用多个字节,称为多字节字符。多字节字符也用char类型表示。
A: 可以把多字节字符转换为宽字符,来生成对应的纹理数据。
不管是英文字符还是汉字字符都转换为宽字符的形式来生成对应的纹理数据。
1、设置本地语言环境
若想要使用宽字符类型,需设置当前语言环境,确保系统和编译器支持。只有正确设置语言环境,才能正确处理数据,否则可能出现乱码。
可使用标准库locale.h中的setlocale函数设置当前语言环境。
setlocale: char *setlocale(itn category, const char *locale)
参数category:已命名的常量,指定设置影响的函数类型。
参数locale:切换到中文环境:Linux:"zh_CN.UTF-8"。Windows:"chs"或""。若locale为空,则根据环境变量值来设置,将程序环境切换为本地化环境。
返回:一个应于区域设置的不透明的字符串。如果请求无效,则返回值是 NULL。
注意:Windows的locale不支持“UTF-8”,可使用GBK,即"chs"(Chinese_People's Republic of China.936)。最好locale使用空字符,切换到本地环境。
2、宽字符使用
使用宽字符类型时,字面量必须在引号前有前缀L。
一个宽字符用单引号' ',一个宽字符字符串即多个宽字符(包括空字符)使用双引号" "。
一个宽字符的占位符为%lc,宽字符字符串的占位符为%ls。
宽字符字符串的结尾符,也占多个字节。
使用头文件wchar.h中的 wprintf 输出宽字符,格式化字符串前必须有"L"。
Windows中也可以用 printf 输出宽字符。但 wprintf 和 printf 不能一起使用。
// 将char类型转化为wchar
// src: 源
// dest: 目标
// locale: 环境变量的值,mbstowcs依赖此值来判断src的编码方式
// 运行成功返回0 否则返回-1
//
int ToWchar(char* &src, wchar_t* &dest, const char *locale = "zh_CN.utf8")
{
if (src == NULL) {
dest = NULL;
return 0;
}
// 根据环境变量设置locale
setlocale(LC_CTYPE, locale);
//setlocale(LC_ALL, "chs");
// 得到转化为需要的宽字符大小
int w_size = mbstowcs(NULL, src, 0) + 1;
// w_size = 0 说明mbstowcs返回值为-1。即在运行过程中遇到了非法字符(很有可能使locale
// 没有设置正确)
if (w_size == 0) {
dest = NULL;
return -1;
}
//wcout << "w_size" << w_size << endl;
dest = new wchar_t[w_size];
if (!dest) {
return -1;
}
int ret = mbstowcs(dest, src, strlen(src) + 1);
if (ret <= 0) {
return -1;
}
return 0;
}
四、shader
static const GLchar * Freetypevertex_source =
"precision mediump float;\n"
"attribute vec4 aPosition;\n"
"varying vec2 vTexCoord;\n"
"uniform mat4 rotateMatrix;\n"
"uniform int uIsUseRotate;\n"
"void main()\n"
"{\n"
"vTexCoord = aPosition.zw;;\n"
"if( uIsUseRotate == 1 )\n"
"{\n"
"gl_Position = vec4(aPosition.xy, 0.0, 1.0) * rotateMatrix;\n"
"}\n"
"else\n"
"{\n"
"gl_Position = vec4(aPosition.xy, 0.0, 1.0);\n"
"}\n"
"}\n";
static const GLchar * Freetypefrag_source =
"precision mediump float;\n"
"uniform sampler2D my_Sampler;\n"
"uniform vec3 textColor;\n"
"varying vec2 vTexCoord;\n"
"void main()\n"
"{\n"
"vec4 sampled = vec4(1.0, 1.0, 1.0, texture2D(my_Sampler, vTexCoord).r);\n"
"gl_FragColor =vec4(textColor, 1.0) *sampled;\n"
"}\n";
五、一行文件的间距设置
// 更新位置到下一个字形的原点,注意单位是1/64像素
x += (ch.Advance >> 6) * scale; //(2^6 = 64)
ch.Advance: 当前字符原点到下一个字形原点的水平距离。
六、位置坐标和纹理坐标更新方式。
左下角:y - (ch.Size.y - ch.Bearing.y) * scale
左上角:(viewport.y-y-23)- (ch.Size.y - ch.Bearing.y) * scale
23值的定义:
使用字库生成文字纹理的大小为26,ch.Size.y - ch.Bearing.y的值为3,26-3=23。
************************************************************/
GLfloat xpos = (x + ch.Bearing.x * scale) / (viewport.x / 2.0) - 1;
GLfloat ypos = ((viewport.y - y - 23) - (ch.Size.y - ch.Bearing.y) * scale) / (viewport.y / 2.0) - 1;
GLfloat w = (ch.Size.x * scale) / (viewport.x / 2.0);
GLfloat h = (ch.Size.y * scale) / (viewport.y / 2.0);
//LOGE("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 }
};