OpenGL点阵字体绘制终极解决方案!

OpenGL点阵字体绘制终极解决方案!哈!

tsuui posted @ 7 years ago in Coding with tags OpenGL 中文显示 freetype , 12160 readers

事情总在变化, opengl迎来了3.3以及4.1的进化, 相信今后的扩充也会朝着这个方向. 对于字体渲染方面, 也并不是什么坏事. 今后有时间再写篇关于3.3和4.1的全屏字体渲染的新方案, 仍然是结合freetype2的, 相信随着freetype2的进步, 和对它的逐步认识, 应该会比现有方案更简单高效... 现在最最最重要的事是...睡觉!!!

对于此文, 大家仅做参考吧.


经过多次修改测试,字体问题终于有了个比较完美的解决方法了,贴出来亮亮~~

此法可以说完全是“红宝书”(即《OpenGL编程指南》)所赐, 此篇也不过是一些实践心得和我自己对字体显示方法的一些体会罢了。

下面就来介绍这个所谓的“终极解决方案”,对于待解决的各种问题,都有着多种可供选择的方案,就让我来边比较边描述吧:

  1. 渲染方式和帧数

不管是不是OpenGL平台, 在每个3D平台中,  点阵字体无非两个用处: 要么做效果,要么做提示。效果就是标题文字、按钮之类的,我们一般称之为banner,titile,caption的东西; 提示就是指一些有动态更新要求的文字,如控制信息提示,  调试模式下的对象名称、坐标等, 还有就是交互场合,比如聊天。

两种应用需求有所不同,但不管是哪种,在OpenGL中我能找到的直接支持字体的,只有三种方法,选择他们的标准只有一个——速度:

● glBindTexture, 纹理贴图,连文字带背景做好一张大图,  按需地选取各个文字子图像,再贴到相应位置的矩形上。贴图能够实现的文字效果最多,你可以把文字纹理映射到空间任意位置的巨型上,可以随意的旋转缩放和变形。在不要求大量动态更新文字内容的地方,可以选用此方法。大部分的小型3D游戏,都采用了这样的方式显示文字,速度够快,能实现所有的变换效果。

不足之处是:

很难实现多颜色混合显示的文字,因为为纹理设置颜色需要的步骤十分繁琐,需要反复切换和设置纹理函数和像素传输转换函数,难免影响性能;

文字内容不能灵活的更换, 除非你打算用很多碎小的纹理来拼凑文章;但随着碎小图片的增多,顶点的和纹理对象也大量增加,需要大量额外的片段处理和过滤操作,会明显拖慢处理流水线,在要求显示大量动态文本的场合下力不从心。不过好在OpenGL在处理纹理对象时多数情况是使用硬件实现的,速度不会慢太多,但也绝对不够块(你可能玩过这样的3D游戏:图像效果场景规模都一般,可鼠标速度慢得难以忍受,出现这种情况,九成的原因是顶点片元过多造成的,单次场景同时显示的纹理片段过碎过多,都会成倍地同时增加顶点和像素片元,拖慢速度,鼠标有时间响应,却没时间画出来);

还有就是变换拉伸后,纹理字体会出现模糊的现象,有些人建议打开Anisotropic Filtering(各向异性过滤)开关, 利用反走样解决,但效果似乎也不稳定,在转角过大、近距离或光线角度太偏的情况下,效果就越来越差了,我想这是纹理映射的通病吧,不可能就一张图你从哪里看都一样的清晰啊,也有人用多等级的纹理和Mipmap解决,本人没试验过(比较麻烦)所以没什么发现权。

● glDrawPixels,像素绘制,任何纹理能够支持的图像格式,它都能支持,缩放也很简单,也可通过设置像素传输和像素封装函数实现一些其他的效果。

缺点是:

他同纹理一样,很难灵活设置颜色; 

只能在光栅上绘制,若需要各种变换效果,还要开辟额外的辅助缓冲和纹理对象;

而最大最大的问题就是速度! 像素在显示之前的处理动作是没有经过加速的,也就是说不管你有没有把他编译到显示列表,像素的转换传输等动作每次都照做不误,它不同于纹理对象中的像素,多数OpenGL实现没有对它开辟专属的显存区域(这种说法有待考证,但实际测试中效率确实很差,编程指南中有特定篇幅介绍了如何提高像素绘制的效率,但即使牺牲一切资源来保证效率,实测效果仍然很难让人满意)。

所以,虽然 glDrawPixels似乎是三种方法中最简单有效的, 可实际运行起来却是三种方法中最慢的!所以如果你要绘制大量点阵字,又想保证帧数的话,宁愿去考虑纹理贴图,也不要在这个函数上花太多心思。

● glBitmap,位图,如果你想在你的3D引擎里添加一个控制台,这个是唯一的选择,96个可打印字符做成位图映射到索引为0x20~0x7F的显示列表,供随时调用。就算直接用glBitmap也来的及,对帧数的影响也不算大,  三种方法中它的速度最能让人满意, 且能通过设置光栅颜色灵活改变位图字体的颜色。想象一下,如果你的控制台里的warning error 普通的log message和user command分别使用了不同的颜色显示,而为实现这个既酷又实用的效果,所付出的代价仅仅是在设置光栅前加个glColor这么简单而已。

缺点:

只能在光栅上绘制,若要缩放旋转之类的变换,需要额外的处理工序,但由于其本身的速度优势,这些工序一般不会对帧数有太大的影响;

另外由于位图只有黑白单色,无法表示灰度,锯齿问题严重,如果只显示英文字体还好,一旦要显示中文,文字效果很差,实在是亵渎中华文化!当然如果你知道怎么在OpenGL里实现一个和ClearType类似的技术,那另当别论。

 

以往对于全屏字体渲染,glBitmap一直是我心中的痛,难以割舍它的高速,又无法忍受它的效果, 直到前一段在读编程指南时,无意间发现了一种利用glBitmap显示反锯齿字体的技巧。当时反复读了几次,貌似明白了上面的意思,拿到机器上试了试, 果然天才, 很好地解决了锯齿的问题,相见恨晚,感叹读书太不认真,怎么早没发现!!  下面简单描述一下这个方法:

对于一副256灰度图像,每个像素使用了一个字节表示0~255个灰度,而位图只有一位0或1,乍一看不太可能,但位图可以灵活设置颜色的特点,成了突破口。既然位图在设置光栅前可以使用glColor为光栅指定"当前光栅颜色",不仅如此,我们还可以指定颜色的alpha值,从而绘制明暗相间的彩色位图,了解了?

把一个反锯齿的灰度字体图像分为多幅位图,假设分为4张位图,第一张:使灰度1~63的相应点置1,其他点置0;第二张:64~127的置1,其他置0...以此类推, 灰阶每上升64的点都集中到同一张位图上。然后,打开混合,使用4次glBitmap调用绘制出来,每次绘制前将光栅颜色设置成与图像对应阶段的灰度,像下面这样: 

GLfloat curColor[4] = { r, g, b, a*0.25f}; //假设当前颜色为 (r,g,b,a) for (int i=0; i<4; ++i) { glColor4fv(curColor); glRasterPosiv(curPos); glBitmap(w,h, 0,0, 0,0, bitmap[i]); //当前alpha增幅0.25, 4次增至1.0 curColor[3] += a*0.25f; } 

就相当于让一张256灰阶的位图降低到5灰阶。这么做的效果如何呢?

下图是我在glut这种超慢框架下的测试的:

中间的截图是用glDrawPixels在打开freetype2的autohinting选项下渲染的256灰阶字体, 上下两张截图都是使用glBitmap绘制的,没有打开autohintng,上面的是3副位图(4灰阶)/字,下面的是4副位图/字。glDrawPixels是使用了显示列表绘制全屏1003个汉字的,已经累成14FPS了,而glBitmap是没用显示列表的,同样1003字一屏,在glut下也能达到50FPS以上! 近乎完美!

(窗口分辨率是960x600)

 同时,由于每个像素变成了4个bit表示(4张图每张1bit),使存储字模所需的空间降至原来的一半。

 

  1. 字库和编码映射

除了glDrawPixels,每一种方法都有应用它的理由,但不管你用哪一种,要克服的最大困难除了渲染速度,就是字库问题了! 读取字库建议使用FreeType2这个开源目, 它支持当今几乎所有流行格式的字体文件,我们可以选择它来作为字体导入的工具,当然也可以把它link到你的程序中,实时的载入ttf字体并按需生成字模图像。解决字库的读取问题,FreeType2绝对是上上之选,就这么简单~

当然, 如果你只想支持普通的96个可打印字符,除了glDrawPixels,其他两种方式随便用——想要效果就用glBindTexture、想要简单方便就glBitmap,然后关掉浏览器、合上参考书,最多半个小时你的字体问题就有着落了! 可如果你想要支持中文??庞大的字库体积是你不得不考虑的另一个问题, 何为庞大?让我们简单地算下:

GB2312编码包含7445个字符,其中汉字6000多个,GBK编码下仅汉字就有20902个,最新国家标准GB18030-2005,总共76546个字符, 而目前的Unicode字符集,已经增至超过10万个字符,虽然现在还没有哪个unicode字库能支持到这么多字符(难道真的有?),但至少20000个还是有的! 而这些字符都是分散在编码空间中的,就是说编码是不连续的,不能使用连续的显示列表索引作简单的映射(即使连续,这么庞大的数目,就算显示列表没有上限,它所占据的显存空间也相当可观),因此不得不为‘字符编码’到‘字模索引/列表索引’建立查找表。

最猛的做法是,在内存平铺整张表,字模全部存入内存,一步索引到字模,生成显示列表,下次再绘制字模时只需索引到显示列表而不必去取字模。这样做好像也没什么问题,没什么问题?如果真的没问题就不会是最猛的了——对于GB2312和GBK这种"小型"多字节编码就需要尽1MB的空间,对于unicode最少最少需要近4MB的空间,而在这个大表里,八成以上的内容是普通人这辈子都用不上的,而每刷新一帧,你的每个要显示的字符都要重复查表一次,在这样大的空间中频繁查表,产生页交换的可能非常的大,速度不慢才怪,绝对不比你每次调用freetype实时转换灰阶来的快,而且还很浪费。

我建议的方法是利用std::map!当然如果你有自己的红黑树类和allocator也可以自己做一个map,效率上可能更胜一筹。map的作用是把字模信息映射到字符编码,动态的载入我们仅有可能用到的那几千个字模信息,这样既节省了空间(省点是点),又比较高效。另外,这里不必专门为map设定空间限制,map在到达一定大小后(大约7000个节点)或每过一段时间后将查找表clear掉就可以了,除非你要在程序里显示《说文解字》全篇,否则要让map增大到5000节点都是个相当有难度的工作。

 

  1. 定制自己的字体文件

哎……这也是被逼无奈,如果你梦想着自己的图行引擎能有全功能的中文支持(显示、输入),你必须一再考虑速度的问题!因为中文实在是太多了……而且万把字符一会要查表一会要转换图像一会又要排布文字,各个环节都不像西文那样方便直接, 都需要额外的繁琐的计算!如果你还要些特效,你一定会比我更吝啬速度。

实践证明,使用了定制点阵字体文件的方式后,不使用显示列表而是实时从内存取得字模再逐个glBitmap,其效率几乎可以和使用了显示列表的内嵌Freetype2的字体系统媲美。至于怎么建立自己的字体文件嘛,我的意见是:怎么方便怎么建,读着方便,用这方便就OK了,因为像这样的位图数据生成文件后数据是很“稀疏”的,很容易压缩和解压,所以空间上不必太担心(我自己做的24×24点阵字体文件,连带额外数据只有4MB多一点)。

其他的就没什么可说的了,要注意的只有三点:你需要一个有序的code-index表,为什么要有序?因为代码域很长而实际的可显示码点很稀少,在一个有序的静态表中二分查找是不二之选;你还需要为每个字模数据建立一个字模信息记录,记录啥?宽width、高height、列步进长度advance、行字节数pitch、字模数据指针等; 还有就是字模数据,如果你想更块一些,让每行像素的字节数扩充到4的倍数,浪费些空间可以再换些速度。

 

到目前为止我们基本完成了下面的要求:

1. 速度快,永远不能放弃对它的追求!

2. 省内存,CPU内存要省,GPU内存更要一省再省!

3. 美观,字是拿来看的,辛勤劳动不能仅因一个难看而被沦为劣质产品。

4. 简单,方法要简单通用!这个好像差点事.....

5. 支持海量中文,在新一轮的‘文字改革’到来之前,这永远是个艰巨的任务!

http://tsuui.is-programmer.com/posts/4252.html

posted on 2015-11-07 08:05 zmj 阅读(2060) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理