C#+OpenGL+FreeType顯示3D文字(1) - 從TTF文件導出字形貼圖
最近需要用OpenGL繪制文字,這是個很費時費力的事。一般的思路就是解析TTF文件從而得到字形的貼圖,然后通過OpenGL繪制貼圖的方式顯示文字。
本篇記錄了解析TTF文件并把所有字形安排到一張大貼圖上的過程。
使用FreeType
想從零開始解析TTF文件是一個比較大的工程,所以目前就借助FreeType。FreeType是一個開源的跨平臺的TTF文件解析器。當然,它還支持解析OpenType等格式的文件。
預備工作
找一個TTF文件
你可以從C:\Windows\Fonts里找到很多TTF文件,我就不提供了。TTF文件里包含了文字的字形。比如下面這兩個就是本篇開頭的兩張貼圖展示的TTF:
下載FreeType源碼
你可以在Github上搜索到FreeType項目,也可以到別的什么地方下載某個版本的FreeType。比如我下載的是2.6版。
源碼下載解壓后如上圖所示。
下載CMake
我想用visual studio打開并編譯它,但它沒有sln工程文件。所以我們先要用CMake來自動生成一個freetype.sln文件。CMake你可以自行百度找到,由于博客園提供的空間有限,我這里也不保留CMake的安裝包了。
安裝完CMake后桌面上會有這樣一個圖標。這就是CMake。
創建FreeType.sln
有了CMake,就可以創建freetype.sln了。
打開CMake,按下圖所示指定所需的參數,即可生成freetype.sln。
下面這一步指定Visual Studio。
看到"Generating Done"就說明成功了。
打開文件夾,可以看到出現了freetype.sln。
編譯生成freetype.dll
有了freetype.sln,用visual studio打開,即可編譯生成freetype.lib。但我們需要的是DLL,怎么辦?按照下述步驟即可實現。
修改宏定義
找到ftoption.h文件,如下圖所示,添加兩個宏定義。
1 #define FT_EXPORT(x) __declspec(dllexport) x 2 #define FT_BASE(x) __declspec(dllexport) x
修改項目配置
如下圖所示,在freetype屬性頁,把配置類型和目標文件擴展名改為.dll。
生成freetype.DLL
現在重新編譯項目,就會生成freetype.dll了。
C#調用FreeType.dll
效果圖
拿到了freetype.dll,下一步就是在C#里調用它。本篇期望用它得到的效果圖已經貼到本文開頭,這里再貼一下:
字形(Glyph)信息
英文單詞Glyph的意思是"字形;圖象字符;縱溝紋"。這就是字形。
一個字形有3個重要的信息:寬度、高度、基線高度。
如上圖所示,從f到k,這是6個字形。
每個字形都用紅色矩形圍了起來。這些紅色的矩形高度對每個字形都是相同的,只有寬度不同。
每個字形還用黃色矩形圍了起來,這些黃矩形的寬度、高度就是字形的寬度、高度。
圖中還有一個藍色的矩形,它的上邊沿與紅色矩形是重合的(為了方便看,我沒有畫重合),它的下邊沿就是"基線"。對于字母"g",它有一部分在基線上方,一部分在基線下方,在基線上方那部分的高度就稱為"基線高度"。
所以,基線高度就是一個字形的黃色矩形框上邊沿到藍色矩形框下邊沿的距離。有了這個基線高度,各個字形才能整齊地排列到貼圖上。
如果沒有基線高度的概念,你看的到貼圖就會是這個樣子:
與上面的效果圖對比一下,你就知道它們的區別了。
封裝freetype.dll
有了上面的基礎,就可以開始干活了。首先要封裝一些freetype相關的類型。這一步比較枯燥且冗長,我把核心類型放在這里,不想看直接跳過即可。

1 public abstract class FreeTypeObjectBase<T> : IDisposable where T : class 2 { 3 /// <summary> 4 /// 指針 5 /// </summary> 6 public IntPtr pointer; 7 8 /// <summary> 9 /// 對象 10 /// </summary> 11 public T obj; 12 13 public override string ToString() 14 { 15 return string.Format("{0}: [{1}]", this.pointer, this.obj); 16 } 17 18 #region IDisposable Members 19 20 /// <summary> 21 /// Internal variable which checks if Dispose has already been called 22 /// </summary> 23 private Boolean disposed; 24 25 /// <summary> 26 /// Releases unmanaged and - optionally - managed resources 27 /// </summary> 28 /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> 29 private void Dispose(Boolean disposing) 30 { 31 if (disposed) 32 { 33 return; 34 } 35 36 if (disposing) 37 { 38 //TODO: Managed cleanup code here, while managed refs still valid 39 } 40 //TODO: Unmanaged cleanup code here 41 ReleaseResource(); 42 this.pointer = IntPtr.Zero; 43 this.obj = null; 44 45 disposed = true; 46 } 47 48 /// <summary> 49 /// Unmanaged cleanup code here 50 /// </summary> 51 protected abstract void ReleaseResource(); 52 53 /// <summary> 54 /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 55 /// </summary> 56 public void Dispose() 57 { 58 // Call the private Dispose(bool) helper and indicate 59 // that we are explicitly disposing 60 this.Dispose(true); 61 62 // Tell the garbage collector that the object doesn't require any 63 // cleanup when collected since Dispose was called explicitly. 64 GC.SuppressFinalize(this); 65 } 66 67 #endregion 68 69 } 70 71 /// <summary> 72 /// FreeType庫 73 /// </summary> 74 public class FreeTypeLibrary : FreeTypeObjectBase<FT_Library> 75 { 76 /// <summary> 77 /// 初始化FreeType庫 78 /// </summary> 79 public FreeTypeLibrary() 80 { 81 int ret = FreeTypeAPI.FT_Init_FreeType(out this.pointer); 82 if (ret != 0) { throw new Exception("Could not init freetype library!"); } 83 84 this.obj = (FT_Library)Marshal.PtrToStructure(this.pointer, typeof(FT_Library)); 85 //lib = Marshal.PtrToStructure<Library>(libptr); 86 } 87 88 protected override void ReleaseResource() 89 { 90 FreeTypeAPI.FT_Done_FreeType(this.pointer); 91 } 92 93 } 94 95 /// <summary> 96 /// 初始化字體庫 97 /// </summary> 98 public class FreeTypeFace : FreeTypeObjectBase<FT_Face> 99 { 100 101 /// <summary> 102 /// 初始化字體庫 103 /// </summary> 104 /// <param name="library"></param> 105 /// <param name="fontFullname"></param> 106 /// <param name="size"></param> 107 public FreeTypeFace(FreeTypeLibrary library, string fontFullname)//, int size) 108 { 109 int retb = FreeTypeAPI.FT_New_Face(library.pointer, fontFullname, 0, out pointer); 110 if (retb != 0) { throw new Exception("Could not open font"); } 111 112 this.obj = (FT_Face)Marshal.PtrToStructure(pointer, typeof(FT_Face)); 113 114 } 115 116 /// <summary> 117 /// Unmanaged cleanup code here 118 /// </summary> 119 protected override void ReleaseResource() 120 { 121 FreeTypeAPI.FT_Done_Face(this.pointer); 122 } 123 124 } 125 126 /// <summary> 127 /// 把字形轉換為紋理 128 /// </summary> 129 public class FreeTypeBitmapGlyph : FreeTypeObjectBase<FT_BitmapGlyph> 130 { 131 /// <summary> 132 /// char 133 /// </summary> 134 public char glyphChar; 135 public GlyphRec glyphRec; 136 137 /// <summary> 138 /// 把字形轉換為紋理 139 /// </summary> 140 /// <param name="face"></param> 141 /// <param name="c"></param> 142 public FreeTypeBitmapGlyph(FreeTypeFace face, char c, int size) 143 { 144 // Freetype measures the font size in 1/64th of pixels for accuracy 145 // so we need to request characters in size*64 146 // 設置字符大小? 147 FreeTypeAPI.FT_Set_Char_Size(face.pointer, size << 6, size << 6, 96, 96); 148 149 // Provide a reasonably accurate estimate for expected pixel sizes 150 // when we later on create the bitmaps for the font 151 // 設置像素大小? 152 FreeTypeAPI.FT_Set_Pixel_Sizes(face.pointer, size, size); 153 154 // We first convert the number index to a character index 155 // 根據字符獲取其編號 156 int index = FreeTypeAPI.FT_Get_Char_Index(face.pointer, Convert.ToChar(c)); 157 158 // Here we load the actual glyph for the character 159 // 加載此字符的字形 160 int ret = FreeTypeAPI.FT_Load_Glyph(face.pointer, index, FT_LOAD_TYPES.FT_LOAD_DEFAULT); 161 if (ret != 0) { throw new Exception(string.Format("Could not load character '{0}'", Convert.ToChar(c))); } 162 163 int retb = FreeTypeAPI.FT_Get_Glyph(face.obj.glyphrec, out this.pointer); 164 if (retb != 0) return; 165 glyphRec = (GlyphRec)Marshal.PtrToStructure(face.obj.glyphrec, typeof(GlyphRec)); 166 167 FreeTypeAPI.FT_Glyph_To_Bitmap(out this.pointer, FT_RENDER_MODES.FT_RENDER_MODE_NORMAL, 0, 1); 168 this.obj = (FT_BitmapGlyph)Marshal.PtrToStructure(this.pointer, typeof(FT_BitmapGlyph)); 169 } 170 171 protected override void ReleaseResource() 172 { 173 //throw new NotImplementedException(); 174 } 175 }
使用freetype時,首先要初始化庫
1 // 初始化FreeType庫:創建FreeType庫指針 2 FreeTypeLibrary library = new FreeTypeLibrary(); 3 4 // 初始化字體庫 5 FreeTypeFace face = new FreeTypeFace(library, this.fontFullname);
之后需要獲取一個字形時,就
1 char c = '&'; 2 int fontHeight = 48; 3 FreeTypeBitmapGlyph glyph = new FreeTypeBitmapGlyph(face, c, fontHeight);
glyph里包含了字形的寬度、高度和用byte表示的灰度圖。
字形集->貼圖
得到glyph就可以生成整個貼圖了,代碼如下。

1 /// <summary> 2 /// 用一個紋理繪制ASCII表上所有可見字符(具有指定的高度和字體) 3 /// </summary> 4 public class ModernSingleTextureFont 5 { 6 private string fontFullname; 7 private char firstChar; 8 private char lastChar; 9 private int maxWidth; 10 private int fontHeight; 11 private int textureWidth; 12 private int textureHeight; 13 Dictionary<char, CharacterInfo> charInfoDict = new Dictionary<char, CharacterInfo>(); 14 15 /// <summary> 16 /// 用一個紋理繪制ASCII表上所有可見字符(具有指定的高度和字體) 17 /// </summary> 18 /// <param name="fontFullname"></param> 19 /// <param name="fontHeight">此值越大,繪制文字的清晰度越高,但占用的紋理資源就越多。</param> 20 /// <param name="firstChar"></param> 21 /// <param name="lastChar"></param> 22 /// <param name="maxWidth">生成的紋理的最大寬度。</param> 23 public ModernSingleTextureFont(string fontFullname, int fontHeight, char firstChar, char lastChar, int maxWidth) 24 { 25 this.fontFullname = fontFullname; 26 this.fontHeight = fontHeight; 27 this.firstChar = firstChar; 28 this.lastChar = lastChar; 29 this.maxWidth = maxWidth; 30 } 31 32 public System.Drawing.Bitmap GetBitmap() 33 { 34 // 初始化FreeType庫:創建FreeType庫指針 35 FreeTypeLibrary library = new FreeTypeLibrary(); 36 37 // 初始化字體庫 38 FreeTypeFace face = new FreeTypeFace(library, this.fontFullname); 39 40 GetTextureBlueprint(face, this.fontHeight, this.maxWidth, out this.textureWidth, out this.textureHeight); 41 42 System.Drawing.Bitmap bigBitmap = GetBigBitmap(face, this.maxWidth, this.textureWidth, this.textureHeight); 43 44 face.Dispose(); 45 library.Dispose(); 46 47 return bigBitmap; 48 } 49 50 private System.Drawing.Bitmap GetBigBitmap(FreeTypeFace face, int maxTextureWidth, int widthOfTexture, int heightOfTexture) 51 { 52 System.Drawing.Bitmap bigBitmap = new System.Drawing.Bitmap(widthOfTexture, heightOfTexture); 53 Graphics graphics = Graphics.FromImage(bigBitmap); 54 55 //for (int i = (int)this.firstChar; i <= (int)this.lastChar; i++) 56 for (char c = this.firstChar; c <= this.lastChar; c++) 57 { 58 //char c = Convert.ToChar(i); 59 FreeTypeBitmapGlyph glyph = new FreeTypeBitmapGlyph(face, c, this.fontHeight); 60 bool zeroSize = (glyph.obj.bitmap.rows == 0 && glyph.obj.bitmap.width == 0); 61 bool zeroBuffer = glyph.obj.bitmap.buffer == IntPtr.Zero; 62 if (zeroSize && (!zeroBuffer)) { throw new Exception(); } 63 if ((!zeroSize) && zeroBuffer) { throw new Exception(); } 64 65 if (!zeroSize) 66 { 67 int size = glyph.obj.bitmap.width * glyph.obj.bitmap.rows; 68 byte[] byteBitmap = new byte[size]; 69 Marshal.Copy(glyph.obj.bitmap.buffer, byteBitmap, 0, byteBitmap.Length); 70 CharacterInfo cInfo; 71 if (this.charInfoDict.TryGetValue(c, out cInfo)) 72 { 73 if (cInfo.width > 0) 74 { 75 System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(cInfo.width, cInfo.height); 76 for (int tmpRow = 0; tmpRow < cInfo.height; ++tmpRow) 77 { 78 for (int tmpWidth = 0; tmpWidth < cInfo.width; ++tmpWidth) 79 { 80 byte color = byteBitmap[tmpRow * cInfo.width + tmpWidth]; 81 bitmap.SetPixel(tmpWidth, tmpRow, Color.FromArgb(color, color, color)); 82 } 83 } 84 85 int baseLine = this.fontHeight / 4 * 3; 86 graphics.DrawImage(bitmap, cInfo.xoffset, 87 cInfo.yoffset + baseLine - glyph.obj.top); 88 } 89 } 90 else 91 { throw new Exception(string.Format("Not support for display the char [{0}]", c)); } 92 } 93 94 } 95 96 graphics.Dispose(); 97 98 return bigBitmap; 99 } 100 101 private void GetTextureBlueprint(FreeTypeFace face, int fontHeight, int maxTextureWidth, out int widthOfTexture, out int heightOfTexture) 102 { 103 widthOfTexture = 0; 104 heightOfTexture = this.fontHeight; 105 106 int glyphX = 0; 107 int glyphY = 0; 108 109 for (int i = (int)this.firstChar; i <= (int)this.lastChar; i++) 110 { 111 char c = Convert.ToChar(i); 112 FreeTypeBitmapGlyph glyph = new FreeTypeBitmapGlyph(face, c, fontHeight); 113 bool zeroSize = (glyph.obj.bitmap.rows == 0 && glyph.obj.bitmap.width == 0); 114 bool zeroBuffer = glyph.obj.bitmap.buffer == IntPtr.Zero; 115 if (zeroSize && (!zeroBuffer)) { throw new Exception(); } 116 if ((!zeroSize) && zeroBuffer) { throw new Exception(); } 117 if (zeroSize) { continue; } 118 119 int glyphWidth = glyph.obj.bitmap.width; 120 int glyphHeight = glyph.obj.bitmap.rows; 121 122 if (glyphX + glyphWidth + 1 > maxTextureWidth) 123 { 124 heightOfTexture += this.fontHeight; 125 126 glyphX = 0; 127 glyphY = heightOfTexture - this.fontHeight; 128 129 CharacterInfo cInfo = new CharacterInfo(); 130 cInfo.xoffset = glyphX; cInfo.yoffset = glyphY; 131 cInfo.width = glyphWidth; cInfo.height = glyphHeight; 132 this.charInfoDict.Add(c, cInfo); 133 } 134 else 135 { 136 widthOfTexture = Math.Max(widthOfTexture, glyphX + glyphWidth + 1); 137 138 CharacterInfo cInfo = new CharacterInfo(); 139 cInfo.xoffset = glyphX; cInfo.yoffset = glyphY; 140 cInfo.width = glyphWidth; cInfo.height = glyphHeight; 141 this.charInfoDict.Add(c, cInfo); 142 } 143 144 glyphX += glyphWidth + 1; 145 } 146 147 } 148 149 }
開源TTF2Bmps下載
根據本篇記錄的內容,我寫了TTF2Bmps這個程序,可以將任何一個TTF文件解析為BMP圖片。你可以在此下載。
2015-08-08



2015-08-10

2015-08-12

總結
有了貼圖,下一步就可以用OpenGL繪制文字了。下回再敘。
文章列表