13.2绘制单个字符


文档摘要

## 13.2 绘制单个字符 这一节主要讲解使用 解析ttf,对字符生成bitmap的流程。 引入freetype到项目 很庞大,一般来说都是编译成library到项目中link。 但是我想参考SDL,尽量不使用library,所以将 直接编译。 由于 代码很多,所以单独创建了 ,然后在项目 进行引用。 freetype流程 使用 对字符生成bitmap的主要流程如下图。 主要分2步: 解析ttf字体 对单个字符生成bitmap 下面进行介绍。 解析ttf字体 游戏中可能使用多种字体,这里创建 类,加载解析字体文件,然后以字体文件路径为key进行存储。 解析ttf字体文件之后,我创建了一张1024的Texture: 。

13.2 绘制单个字符

CLion项目文件位于 samples\draw_font\draw_ttf_font_freetype

这一节主要讲解使用freetype解析ttf,对字符生成bitmap的流程。

1. 引入freetype到项目

freetype很庞大,一般来说都是编译成library到项目中link。

但是我想参考SDL,尽量不使用library,所以将freetype直接编译。

由于freetype代码很多,所以单独创建了CMakeLists.txt.FreeType,然后在项目CMakeLists.txt进行引用。

2. freetype流程

使用freetype对字符生成bitmap的主要流程如下图。

主要分2步:

  1. 解析ttf字体
  2. 对单个字符生成bitmap

下面进行介绍。

3. 解析ttf字体

游戏中可能使用多种字体,这里创建Font类,加载解析字体文件,然后以字体文件路径为key进行存储。

//file:source/renderer/font.cpp line:17 /// 加载一个字体文件并解析 /// \param image_file_path ttf字体文件路径 /// \param font_size 默认文字尺寸 /// \return Font* Font::LoadFromFile(std::string font_file_path,unsigned short font_size){ Font* font=GetFont(font_file_path); if(font!= nullptr){ return font; } //读取 ttf 字体文件 ifstream input_file_stream(Application::data_path()+ font_file_path,ios::in | ios::binary); input_file_stream.seekg(0,std::ios::end); int len = input_file_stream.tellg(); input_file_stream.seekg(0,std::ios::beg); char *font_file_buffer = new char[len]; input_file_stream.read(font_file_buffer , len); //将ttf 传入FreeType解析 FT_Library ft_library= nullptr; FT_Face ft_face= nullptr; FT_Init_FreeType(&ft_library);//FreeType初始化; FT_Error error = FT_New_Memory_Face(ft_library, (const FT_Byte*)font_file_buffer, len, 0, &ft_face); if (error != 0){ spdlog::error("FT_New_Memory_Face return error {}!",error); return nullptr; } FT_Select_Charmap(ft_face, FT_ENCODING_UNICODE); FT_F26Dot6 ft_size = (FT_F26Dot6)(font_size*(1 << 6)); FT_Set_Char_Size(ft_face, ft_size, 0, 72, 72); if (ft_face == nullptr){ spdlog::error("FT_Set_Char_Size error!"); return nullptr; } //创建Font实例,保存Freetype解析字体结果。 font=new Font(); font->font_size_=font_size; font->font_file_buffer_=font_file_buffer; font->ft_library_=ft_library; font->ft_face_=ft_face; font_map_[font_file_path]=font; //创建空白的、仅Alpha通道纹理,用于生成文字。 unsigned char * pixels = (unsigned char *)malloc(font->font_texture_size_ * font->font_texture_size_); memset(pixels, 0,font->font_texture_size_*font->font_texture_size_); font->font_texture_=Texture2D::Create(font->font_texture_size_,font->font_texture_size_,GL_RED,GL_RED,GL_UNSIGNED_BYTE,pixels); delete pixels; return font; }

freetype解析ttf字体文件之后,我创建了一张1024的Texture:font->font_texture_

这其实就是一张图集,和普通的小图打包成大图唯一的不同之处就是,这里的小图是freetype动态生成的bitmap。

freetype为字符动态生成bitmap后,可以使用OpenGL提供的API,对图集进行局部更新。

Unity中对UI做DrawCall合并时,也有动态图集的做法,差不多的逻辑。

下面就来看下如何为字符动态生成bitmap吧。

4. 对单个字符生成bitmap

//file:source/renderer/font.cpp line:70 /// freetype为字符生成bitmap /// \param c void Font::LoadCharacter(char ch) { //加载这个字的字形,加载到 m_FTFace上面去;Glyph:字形,图形字符 [glif]; FT_Load_Glyph(ft_face_, FT_Get_Char_Index(ft_face_, ch), FT_LOAD_DEFAULT); //从 FTFace上面读取这个字形 到 ft_glyph 变量; FT_Glyph ft_glyph; FT_Get_Glyph(ft_face_->glyph, &ft_glyph); //渲染为256级灰度图 FT_Glyph_To_Bitmap(&ft_glyph, ft_render_mode_normal, 0, 1); FT_BitmapGlyph ft_bitmap_glyph = (FT_BitmapGlyph)ft_glyph; FT_Bitmap& ft_bitmap = ft_bitmap_glyph->bitmap; font_texture_->UpdateSubImage(0, 0, ft_bitmap.width, ft_bitmap.rows, GL_RED, GL_UNSIGNED_BYTE, ft_bitmap.buffer); }

FT_Bitmap结构里就保存着freetype为字符生成的bitmap,从它里面拿到bitmap尺寸、二进制数据,就可以上传到GPU对大的图集局部更新。

5. 创建材质

创建一个新的材质data/material/quad_draw_font.mat

使用图片进行渲染时,需要在材质中设置图片文件路径。

而字体是生成的bitmap,所以在材质中将图片文件路径留空:image=""

<material shader="shader/unlit"> <texture name="u_diffuse_texture" image=""/> </material>

6. 渲染文字

有了文字Texture,也有了Material,只需要创建顶点数据、索引数据就可以渲染了。

//file:example/login_scene.cpp line:70 /// 创建文字 void LoginScene::CreateFont() { vector<MeshFilter::Vertex> vertex_vector={ { {-1.0f, -1.0f, 1.0f}, {1.0f,1.0f,1.0f,1.0f}, {0.0f, 0.0f} }, { { 1.0f, -1.0f, 1.0f}, {1.0f,1.0f,1.0f,1.0f}, {1.0f, 0.0f} }, { { 1.0f, 1.0f, 1.0f}, {1.0f,1.0f,1.0f,1.0f}, {1.0f, 1.0f} }, { {-1.0f, 1.0f, 1.0f}, {1.0f,1.0f,1.0f,1.0f}, {0.0f, 1.0f} } }; vector<unsigned short> index_vector={ 0,1,2, 0,2,3 }; //创建模型 GameObject auto go=new GameObject("quad_draw_font"); go->set_layer(0x01); //挂上 Transform 组件 auto transform=dynamic_cast<Transform*>(go->AddComponent("Transform")); transform->set_position({2.f,0.f,0.f}); //挂上 MeshFilter 组件 auto mesh_filter=dynamic_cast<MeshFilter*>(go->AddComponent("MeshFilter")); mesh_filter->CreateMesh(vertex_vector,index_vector); //创建 Material material=new Material();//设置材质 material->Parse("material/quad_draw_font.mat"); //挂上 MeshRenderer 组件 auto mesh_renderer=dynamic_cast<MeshRenderer*>(go->AddComponent("MeshRenderer")); mesh_renderer->SetMaterial(material); //生成文字贴图 Font* font=Font::LoadFromFile("font/hkyuan.ttf",500); font->LoadCharacter('A'); //使用文字贴图 material->SetTexture("u_diffuse_texture", font->font_texture()); }

7. 测试

运行项目测试,结果如下图:

可以看到茶壶旁边的大A,不过它是上下颠倒的,是哪里出错了吗?

打开RenderDoc对程序进行截屏分析,就可以看到显存里的文字图集,它确实是反的。

这是因为FreeType将左上角作为坐标原点,而OpenGL是左下角。

所以需要将UV坐标翻转,这里就不做了,下一节会翻转过来。

另外一个问题是,为什么文字是红色的?

这是因为FreeType生成的是bitmap,作为单通道即可,而OpenGL对单通道可选的format只有GL_RED,所以在创建Texture的时候就使用了GL_RED,那么在片段Shader中就读取到R通道,就是RED,红色通道。

font->font_texture_=Texture2D::Create(font->font_texture_size_,font->font_texture_size_,GL_RED,GL_RED,GL_UNSIGNED_BYTE,pixels);

8. 相关参考

主要参考OpenGL API文档。

  1. https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml
  2. https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glTexSubImage2D.xhtml

9. RenderDoc分析不显示bug

在编写本节教程时,遇到了文字不显示的Bug,最终使用RenderDoc分析出是C++临时变量的问题。

具体分析过程查看后面章节:[90.1 RenderDoc分析不显示bug](90. gpu_analysis_tools/90.1 renderdoc_analysis_not_show_bugs.md)


发布者: 作者: 转发
评论区 (0)
U