Linux OpenGL 实践篇-16 文本绘制
文本绘制
本文主要射击Freetype的入门理解和在OpenGL中实现文字的渲染。
OpenGL本身并没有提供文本绘制功能,通常有两种方式来绘制文本,
- 第一种是使用显示列表,即为每一个字符创建一个显示列表,绘制的时候调用对应的显示列表即可;
- 第二种就是纹理字体,即为每一个字符创建一个纹理,绘制的时候就和普通的物体绘制一样;
使用FreeType绘制文本属于纹理字体的方式,这种绘制方式的好处就是灵活,扩展性强,比如你想把文字绘制在一个飘动的红旗上,用显示列表的方式就很难实现;
demo网址:https://learnopengl-cn.github.io/06%20In%20Practice/02%20Text%20Rendering/。
FreeType
freetype的官网:https://www.freetype.org/freetype2/docs/tutorial/step1.html#section-2
library
FreeType中的library其类型是FT_Library,定义如下:
typedef struct FT_LibraryRec_ *FT_Library;
所以可以简单的理解为一个FT_LibraryRec_的对象,虽然FreeType用c写的,但这个地方不妨碍我们使用对象来理解它。因为_LibraryRec_我并没有看到源代码,具体包含哪些内容我并不清楚。但根据其用法,可以推测其应该是一些字体上下文的内容,比如缓存、内存管理等。
FT_Error = FT_Init_Freetype(&library);
face
一个face可以理解为字体的描述或者说字形的集合,比如“Times New Roman Regular"表示正常的新罗马字体,而"Times New Roman Italic"表示新罗马字体的斜体表示。一个字体文件中可以嵌入多个face,我们可以通过下面的API加载特定的face:
FT_Error FT_New_Face(FT_Library library,const char* filepathname,FT_Long face_index,FT_Face *aface);
其中face_index就表示要加载字体文件中的哪个face,一般情况下,0总是可用的。如果face_index设置为-1,则可以通过face->num_faces获取字体文件中有多少个face。
error = FT_New_Face(library,"./arial.ttf",0,&face);
字体大小
error = FT_Set_Char_Size( face, /* handle to face object */ 0, /* char_width in 1/64th of points */ 16*64, /* char_height in 1/64th of points */ 300, /* horizontal device resolution */ 300 ); /* vertical device resolution */
在这里有两个概念需要注意:pt和dpi。pt是point的缩写,可以立即为一个点,一个点的大小使用物理距离来描述的,通常是1/72英寸。dpi的意思是dot per inch,表示每英寸有多少个点,这个点表示的是显示设备最小输出单元,可以理解为我们常说的像素,主流显示设备的标准值是72dpi和96dpi,这个可以在显示器配置中查到。所以在这个方法中通过同时指定pt大小和设备的dpi,根据换算关系可以计算字符的像素大小。
下面的代码直接设置字形的像素大小。注意这两个方法中,width的值设置为0,表示width与height一样,height设置为0同理。
error = FT_Set_Pixel_Sizes( face, /* handle to face object */ 0, /* pixel_width */ 16 ); /* pixel_height */
glyph
字形,其实就是某个字符具体的形状描述。字形的描述有两种,一种是位图,一种是矢量图。位图又叫点阵图,简单理解就是图像是由一个一个像素点组成,每个像素点可以有自己的颜色;矢量图则记录的是一个一个的对象,这些对象是一种形状,形状由数学公式来描述的,简单的理解就是矢量图记录的是图像的画法。这两种描述最大的区别就是,缩放的时候矢量图不会失真。
使用FreeType的时候,我们不需要关心这些底层的实现,直接使用FT_Load_Glyph即可加载。
FT_UInt FT_Get_Char_Index( FT_Face face, FT_ULong charcode ); FT_Error FT_Load_Glyph( FT_Face face, FT_UInt glyph_index, //The index of the glyph in the font file. FT_Int32 load_flags ); //A flag indicating what to load for this glyph
FT_Error FT_Render_Glyph( face->glyph, /* glyph slot */
render_mode ); /* render mode */
字形的数据结构:
typedef struct FT_GlyphSlotRec_ { FT_Library library; FT_Face face; FT_GlyphSlot next; FT_UInt reserved; /* retained for binary compatibility */ FT_Generic generic; FT_Glyph_Metrics metrics; FT_Fixed linearHoriAdvance; FT_Fixed linearVertAdvance; FT_Vector advance; FT_Glyph_Format format; FT_Bitmap bitmap; FT_Int bitmap_left; FT_Int bitmap_top; FT_Outline outline; FT_UInt num_subglyphs; FT_SubGlyph subglyphs; void* control_data; long control_len; FT_Pos lsb_delta; FT_Pos rsb_delta; void* other; FT_Slot_Internal internal; } FT_GlyphSlotRec;
在字形数据结构中,几个比较重要的数据是bitmap,bitmap_left,bitmap_top。其中bitmap其实可以理解为字形的位图数据,定义如下:
typedef struct FT_Bitmap_ { unsigned int rows; unsigned int width; int pitch; unsigned char* buffer; unsigned short num_grays; unsigned char pixel_mode; unsigned char palette_mode; void* palette; } FT_Bitmap;
其中buffer像素数据,rows,width表示图像的宽和高,我们在OpenGL中绘制时主要用到的三个数据。
除了bitmap,还有几个重要的数据,这写数据主要影响多个文本之间的布局,因为不是每个字形的大小都是一样的,比如bitmap_left,bitmap_right等。让我们以下图为例说明一下FreeType对于字形的描述:
每一个字形都放在一个水平的基准线(Baseline)上(即上图中水平箭头指示的那条线)。一些字形恰好位于基准线上(如’X’),而另一些则会稍微越过基准线以下(如’g’或’p’)(译注:即这些带有下伸部的字母,可以见这里)。这些度量值精确定义了摆放字形所需的每个字形距离基准线的偏移量,每个字形的大小,以及需要预留多少空间来渲染下一个字形。下面这个表列出了我们需要的所有属性。
在OpenGL中绘制文字
FreeType 官网有些例子给我们参考,点击这里查看。其中一个简单的例子是在终端中以字符的形式输出字形,这个例子可以帮助我们理解bitmap中的数据形式。
获取到字形的bitmap后,在OpenGL绘制文字的思路就很简单了:根据字形数据生成二维纹理,然后把这个纹理绘制到一个四方形中,以下是绘制的主要代码,完整代码见:https://github.com/xin-lover/opengl-learn/tree/master/chapter-16-glfw/chapter-font。
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); //禁用字节对齐限制
unsigned int textureID;
glGenTextures(1,&textureID);
glBindTexture(GL_TEXTURE_2D,textureID);
glTexImage2D(GL_TEXTURE_2D,0,format,width,height,0,format,GL_UNSIGNED_BYTE,data);
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);
在这里需要注意字节对齐的问题,OpenGL默认要求纹理4字节对齐,即纹理的大小是4的倍数,这通常不会有什么问题,因为绝大多数的纹理大小都是4的倍数并/或每个橡树4字节大小,但现在我们一个像素是一个字节(GL_RED),它可以是任意的宽度,所以需要需求这个限制,将对齐参数设置为1,不然的话可能会造成段错误。
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);//禁用字节对齐限制
效果: