游戏引擎 浅入浅出

Introduction

前言












12. 拆分引擎和项目















89. Doxygen生成API文档





代码资源下载


目录

13.2 绘制单个字符

  1. 「游戏引擎 浅入浅出」是一本开源电子书,PDF/随书代码/资源下载: https://github.com/ThisisGame/cpp-game-engine-book
  1. 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进行存储。

  1. //file:source/renderer/font.cpp line:17
  2. /// 加载一个字体文件并解析
  3. /// \param image_file_path ttf字体文件路径
  4. /// \param font_size 默认文字尺寸
  5. /// \return
  6. Font* Font::LoadFromFile(std::string font_file_path,unsigned short font_size){
  7. Font* font=GetFont(font_file_path);
  8. if(font!= nullptr){
  9. return font;
  10. }
  11. //读取 ttf 字体文件
  12. ifstream input_file_stream(Application::data_path()+ font_file_path,ios::in | ios::binary);
  13. input_file_stream.seekg(0,std::ios::end);
  14. int len = input_file_stream.tellg();
  15. input_file_stream.seekg(0,std::ios::beg);
  16. char *font_file_buffer = new char[len];
  17. input_file_stream.read(font_file_buffer , len);
  18. //将ttf 传入FreeType解析
  19. FT_Library ft_library= nullptr;
  20. FT_Face ft_face= nullptr;
  21. FT_Init_FreeType(&ft_library);//FreeType初始化;
  22. FT_Error error = FT_New_Memory_Face(ft_library, (const FT_Byte*)font_file_buffer, len, 0, &ft_face);
  23. if (error != 0){
  24. spdlog::error("FT_New_Memory_Face return error {}!",error);
  25. return nullptr;
  26. }
  27. FT_Select_Charmap(ft_face, FT_ENCODING_UNICODE);
  28. FT_F26Dot6 ft_size = (FT_F26Dot6)(font_size*(1 << 6));
  29. FT_Set_Char_Size(ft_face, ft_size, 0, 72, 72);
  30. if (ft_face == nullptr){
  31. spdlog::error("FT_Set_Char_Size error!");
  32. return nullptr;
  33. }
  34. //创建Font实例,保存Freetype解析字体结果。
  35. font=new Font();
  36. font->font_size_=font_size;
  37. font->font_file_buffer_=font_file_buffer;
  38. font->ft_library_=ft_library;
  39. font->ft_face_=ft_face;
  40. font_map_[font_file_path]=font;
  41. //创建空白的、仅Alpha通道纹理,用于生成文字。
  42. unsigned char * pixels = (unsigned char *)malloc(font->font_texture_size_ * font->font_texture_size_);
  43. memset(pixels, 0,font->font_texture_size_*font->font_texture_size_);
  44. font->font_texture_=Texture2D::Create(font->font_texture_size_,font->font_texture_size_,GL_RED,GL_RED,GL_UNSIGNED_BYTE,pixels);
  45. delete pixels;
  46. return font;
  47. }

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

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

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

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

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

4. 对单个字符生成bitmap

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

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

5. 创建材质

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

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

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

  1. <material shader="shader/unlit">
  2. <texture name="u_diffuse_texture" image=""/>
  3. </material>

6. 渲染文字

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

  1. //file:example/login_scene.cpp line:70
  2. /// 创建文字
  3. void LoginScene::CreateFont() {
  4. vector<MeshFilter::Vertex> vertex_vector={
  5. { {-1.0f, -1.0f, 1.0f}, {1.0f,1.0f,1.0f,1.0f}, {0.0f, 0.0f} },
  6. { { 1.0f, -1.0f, 1.0f}, {1.0f,1.0f,1.0f,1.0f}, {1.0f, 0.0f} },
  7. { { 1.0f, 1.0f, 1.0f}, {1.0f,1.0f,1.0f,1.0f}, {1.0f, 1.0f} },
  8. { {-1.0f, 1.0f, 1.0f}, {1.0f,1.0f,1.0f,1.0f}, {0.0f, 1.0f} }
  9. };
  10. vector<unsigned short> index_vector={
  11. 0,1,2,
  12. 0,2,3
  13. };
  14. //创建模型 GameObject
  15. auto go=new GameObject("quad_draw_font");
  16. go->set_layer(0x01);
  17. //挂上 Transform 组件
  18. auto transform=dynamic_cast<Transform*>(go->AddComponent("Transform"));
  19. transform->set_position({2.f,0.f,0.f});
  20. //挂上 MeshFilter 组件
  21. auto mesh_filter=dynamic_cast<MeshFilter*>(go->AddComponent("MeshFilter"));
  22. mesh_filter->CreateMesh(vertex_vector,index_vector);
  23. //创建 Material
  24. material=new Material();//设置材质
  25. material->Parse("material/quad_draw_font.mat");
  26. //挂上 MeshRenderer 组件
  27. auto mesh_renderer=dynamic_cast<MeshRenderer*>(go->AddComponent("MeshRenderer"));
  28. mesh_renderer->SetMaterial(material);
  29. //生成文字贴图
  30. Font* font=Font::LoadFromFile("font/hkyuan.ttf",500);
  31. font->LoadCharacter('A');
  32. //使用文字贴图
  33. material->SetTexture("u_diffuse_texture", font->font_texture());
  34. }

7. 测试

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

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

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

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

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

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

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

  1. 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

Introduction

前言












12. 拆分引擎和项目















89. Doxygen生成API文档