C#+OpenGL+FreeType显示3D文字(1) - 从TTF文件导出字形贴图
最近需要用OpenGL绘制文字,这是个很费时费力的事。一般的思路就是解析TTF文件从而得到字形的贴图,然后通过OpenGL绘制贴图的方式显示文字。
本篇记录了解析TTF文件并把所有字形安排到一张大贴图上的过程。
想从零开始解析TTF文件是一个比较大的工程,所以目前就借助FreeType。FreeType是一个开源的跨平台的TTF文件解析器。当然,它还支持解析OpenType等格式的文件。
你可以从C:\Windows\Fonts里找到很多TTF文件,我就不提供了。TTF文件里包含了文字的字形。比如下面这两个就是本篇开头的两张贴图展示的TTF:
你可以在Github上搜索到FreeType项目,也可以到别的什么地方下载某个版本的FreeType。比如我下载的是2.6版。
源码下载解压后如上图所示。
我想用visual studio打开并编译它,但它没有sln工程文件。所以我们先要用CMake来自动生成一个freetype.sln文件。CMake你可以自行百度找到,由于博客园提供的空间有限,我这里也不保留CMake的安装包了。
安装完CMake后桌面上会有这样一个图标。这就是CMake。
有了CMake,就可以创建freetype.sln了。
打开CMake,按下图所示指定所需的参数,即可生成freetype.sln。
下面这一步指定Visual Studio。
看到"Generating Done"就说明成功了。
打开文件夹,可以看到出现了freetype.sln。
有了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#里调用它。本篇期望用它得到的效果图已经贴到本文开头,这里再贴一下:
英文单词Glyph的意思是"字形;图象字符;纵沟纹"。这就是字形。
一个字形有3个重要的信息:宽度、高度、基线高度。
如上图所示,从f到k,这是6个字形。
每个字形都用红色矩形围了起来。这些红色的矩形高度对每个字形都是相同的,只有宽度不同。
每个字形还用黄色矩形围了起来,这些黄矩形的宽度、高度就是字形的宽度、高度。
图中还有一个蓝色的矩形,它的上边沿与红色矩形是重合的(为了方便看,我没有画重合),它的下边沿就是"基线"。对于字母"g",它有一部分在基线上方,一部分在基线下方,在基线上方那部分的高度就称为"基线高度"。
所以,基线高度就是一个字形的黄色矩形框上边沿到蓝色矩形框下边沿的距离。有了这个基线高度,各个字形才能整齐地排列到贴图上。
如果没有基线高度的概念,你看的到贴图就会是这个样子:
与上面的效果图对比一下,你就知道它们的区别了。
有了上面的基础,就可以开始干活了。首先要封装一些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这个程序,可以将任何一个TTF文件解析为BMP图片。你可以在此下载。
有了贴图,下一步就可以用OpenGL绘制文字了。下回再叙。