最近学习了一下Windows下的字体渲染,写个文档记录一下成果,方便回顾和分享。要在Windows平台下渲染字体主要有三种途径: b( A* G0 d$ ]" F
2 @- m$ T* y; R @4 S8 X
1. 调用Windows SDK提供的图形接口GDI/GDI+。% V' D# C: \# e% j
! f( z1 G6 \1 Y) h( y1 z. @2. 调用DirectX提供的 D3DXCreateText 或是ID3DXFont的相关接口来实现。 [2 I$ S( p8 w: r3 m- {- T" g/ B
, |6 y) l7 e) C2 q: D" l8 f
3. 用GDI或是其他第三方库(如Freetype)渲染出字形,再调用DX或SDK来绘制图形。
/ N j" ^6 n- Y9 J/ _! b9 Y4 \1 e
这三种方式各自有各的定位,不能简单地来哪个方法好,哪个方法坏。- y6 ~( W* j! ]* u! b4 \8 L' l+ O
! t# K2 [$ [7 ? z/ L
. o% k* E8 C4 s1 _ r8 F: l& }
; i) U' U0 Q+ E a
9 U- f+ u4 E7 }, g0 |
, G5 G0 Z# l( w5 I" k' A$ l一、 GDI / GDI+方案$ k* j- i0 \( c/ U8 L4 Q
8 O; X7 e" X/ O' D% L
具有方便,低效的特点,内置简单排版功能,适合一般应用程序开发需求。
' U" B$ ~1 d2 u$ w3 B: W7 s8 Y% x3 |. N4 r [) p7 D7 F, H" h
Windows SDK提供的GDI / GDI+是为Windows平台下开发主流应用程序设计的,GDI提供了CreateFont,DrawText,TextOut等一系列方法以及GDI+提供的Font类使得客户程序员能够很方便地调用并实现应用程序字体渲染的目的。对于一般应用程序来说,只有当应用程序窗口需要重新绘制整个窗口的时候才需要去调用相应的接口重新渲染文字(如窗口从最小化的状态恢复或是大小改变或是被其他窗口覆盖或是被拖出屏幕边缘等),这些应用的调用频率都很低,对效率的要求不高,在这种需求下,使用GDI/GDI+提供的文字绘制接口是最合适的,方便而且强大,能渲染粗体、斜体等,而且还内置排版功能。但是如果是游戏开发,我想任何开发者都不会去直接使用GDI/GDI+提供的接口,原因是效率太低了,而且自定义程度不够,不能渲染游戏中需要用到的特殊效果文字。和其他应用程序不同,游戏的窗口是时时刻刻在重绘的,游戏中的帧数也就是每秒重绘的次数,如果帧数较低,就会觉得游戏画面卡,玩家体验会很差,这种需求就决定了字体渲染必须是高效的并且能自定义效果的(如阴影,描边等)。% u7 X% d0 I9 T
' j! S; ?0 r7 y9 \8 R/ a+ q
但是GDI/GDI+为什么不够高效呢?是微软实现的太烂了?当然不是,从设计上讲微软提供这些接口都是面向一般应用程序需求的,没有高效的需求,他只要简单,方便使用,能渲染一般应用程序会用到的字体就行了,所以相对早期GDI来说,GDI+更加低效,但是更加方便,当然这也有早期硬件条件的限制。从程序实现上考虑,低效的原因是接口内部没有做数据缓存,当你调用了DrawText或TextOut等方法绘制文字时,他会根据你传入的文字重新生成字形的图形数据并将它绘制到设备上(HDC),而由字体编码到字形的图形数据这一步操作是相当慢的,程序首先会根据字符的编码去字体文件的Charmap表中查找该编码对应的字形数据位置,再读取相关的数据(可能是Bezier曲线或是B样曲线数据,点阵数据),根据参数计算出字形,最终将它转换成Bitmap数据。如果GDI/GDI+把最终得到的图形数据以字为单位缓存在一个表中,下次需要再次绘制该文字的时候直接取现存的数据,效率就能提高很多,但是他没这么做。
( [: s- P8 u0 f
$ P5 B3 v; R2 g6 r" F+ i $ M% X; @ A& b( d7 `( q1 D
% t2 O2 Y& {9 o. X% `. e
2 X# }! f3 O5 Q& l. o& H
' n0 R% T9 G: n# E二、 D3DCreateText
+ n) D$ s* P T2 ]' \4 Y3 }3 g) A5 N3 j/ _- o* k4 M7 B5 d5 S' c
能够渲染3D文字,只有特殊应用中会用到,需要DX支持。" I& P5 F7 ]$ x' G
* j) {4 P! D3 U% K
这是 DirectX的扩展接口,能够创建Mesh。 D3DCreateText从字体文件中获取字形数据,并把每个字渲染成N个三角形(每个字大概在100~400个之间),把所有顶点数据保存成一个Mesh并作为执行结果返回,最后直接调用Mesh接口的绘制。很显然,这个接口的效率不高,所以除非需要用3D文字,一般不会去调用。可能有人会想:如果以字为单位去生成Mesh并且缓存起来,效率就会高了。其实不见得,虽然这样做确实省下了重复生成Mesh的开销,但是大大降低了显卡渲染速度(因为显卡对大量顶点的批量处理是很快的),所以总体性能不会有所提升,可能反而会更加低。当然,还有其他办法能综合以上优缺点,从Mesh中取出顶点数据自己做二次处理,这里就不展开了。
" g3 `, x( Y. Z/ |
# Y4 ~. y+ ~' `- C6 [ ) k6 j4 D3 o8 q" B7 Z; s. Q
+ u V: u; v+ d6 Z W
# _+ D9 l0 r. }4 s4 s |8 i0 j8 }! K$ c
三、 ID3DXFont
; E/ b. ]) u! g/ l2 N4 E, m% P" f9 n+ S- M
相比GDI/GDI+具有高效的特点,接口也很方便使用,但需要DX支持。
+ c3 d* r9 _/ S3 o7 n
# `, P* F- h A( m4 j1 XID3DXFont字形生成的实现其实是调用了GDI的接口,但是为什么比GDI高效呢?原因在于他做了纹理(Texture)缓存。ID3DXFont会把生成的图形数据以字为单位保存到若干张纹理中,在绘制某个字时,先查找缓存中有没现成的,如果没有则调用GDI去生成该字的图形数据并缓存到纹理中去。这样做效率确实比GDI高了很多,但是经测试,性能还不够理想,在Debug版本下,全速渲染600左右中文和800左右的英文(有重复)平均帧数为114,用DirectX SDK自带的工具PIX for Windows可以很容易找到性能不够理想的原因。从PIX显示的数据中可以看出,ID3Dfont的DrawText方法在绘制每个文字的时候,会先锁住显存中的顶点缓冲区,然后再将顶点数据写入缓冲区中,再次调用顶点渲染函数。这样每次加锁都会中断GPU的渲染流水线,等写完数据解锁以后才能继续,直接导致了性能下降。知道了性能不高的根本原因,解决就比较容易了,我们可以在内存中自己建一个比较大的顶点缓冲区,把需要在一帧中渲染的顶点数据先写入到这个顶点缓冲区中,在调用渲染接口一次性渲染所有顶点,这样能使性能提升好几倍。在上次的测试环境中用新的方案测试了一下,测试数据不变,能够达到平均784帧的速度,效果还是很明显的。这个速度用于开发游戏,还是不错的。3 J8 b' G3 h4 ?. q
( r/ u9 Y: M3 i0 [: o; Z6 g, a这样性能上确实可以接受了,但是还存在着其他一些问题。ID3DXFont对应了特定样式的一个字体,比如微软雅黑12号字对应一个实力,而微软雅黑12号字的斜体也必须对应另外一个实例。ID3DXFont中所使用到的纹理贴图都是该接口内部管理的,这给客户程序员带来了很大的方便(不用管理纹理了,而且他本身的贴图拼接的算法也很不错),当然随之而来的是你没法写入自己的贴图数据(除非自己再建贴图来管理,但是这样实现感觉上很乱)。比如需要渲染具有描边效果的文字,用ID3DXFont自己管理纹理的方式你就需要把文字以描边的颜色在上下左右等八个方向偏移若干个像素都绘制一次,再在用字体颜色在原坐标上绘制该文字才能实现描边效果,总共绘制了九遍。当然这在少量描边字的情况下性能还是能接受的。除了多次绘制,还有其他更高效的办法实现描边吗?有,就是在绘制之前专门生成在一块描边文字的边的纹理贴图,在绘制的时候,先绘制描边,在绘制文字本身,也能实现描边的效果。这种办法当然能提高效率,但是会需要更多的显存来放纹理,其实本质上是一种以空间换时间的办法,是否有必要还得看实际情况,而且还有一点就是你必须自己来管理这些纹理贴图。
$ `/ g$ ?% N1 O- q) P
; b: z1 n- l) e n' O; n 2 \2 J, F6 p* v* @- H% w z& d
i$ | m Z7 O% P0 V
0 t9 v7 U& T6 b8 |) H
& Z5 \5 R/ Y8 B四、 Freetype
' {) H1 q( o6 s4 m s9 H" I- O) l# }2 [8 O8 c$ }) d6 w
高效,能最大程度自定义,但是实现比较麻烦,适合游戏开发应用,需要DX支持。% z: {' e( z9 v' {. _+ Z
9 F7 F1 S# E* A# l) Z4 N9 D( P
Freetype是个开源的可以自由使用的多平台文字渲染的库,特点是占用空间小、高效、高度可定制、并且可以产生可移植的高品质输出(符号图像), 他是一种字体服务而没有提供为实现文字布局或图形化处理这样高阶的功能使用的API(比如带色文字渲染之类的)。通俗点说他只负责将文字编码转换成字形数据。Freetype也不支持粗体,斜体等附加效果的渲染。但是Freetype支持矢量字形的输出,如果对高质量的缩放文字有需求的话,他将是个很好的选择。GDI也有这些实现GetFontData和GetGlyphOutline就能获取字形数据,而且比Freetype功能更加强大,GDI能渲染粗体,斜体等附加效果。# H9 q* K4 {. I; M" i* @4 y
4 e7 g3 r( S8 z6 K6 u9 m2 `; P. o
通过Freetype或是GDI得到图形数据之后,我们就可以用DX创建纹理贴图来缓存这些数据,方法就和其他方式下差不多了,只是这种方式下得自己管理贴图,自己处理顶点缓冲区(优化),瑞然实现比较麻烦,但是性能和自定义的程度是最大化的,纹理贴图在自己的控制中,想附加什么效果都可以,不再啰嗦。$ a; G' [6 ?2 A3 I% _! N9 _) G1 _. @
?2 c+ J1 _/ X' O( E4 E & m9 ?7 w8 Z" U% Z; M- Q
" f4 M3 C, b4 u" o* F0 K
最后再提一点关于粗体和斜体这些附加效果的渲染方法,粗体其实可以用描边的方式来实现,只要边的颜色和字体颜色一样,看上去就是粗体的效果,至于斜体,其实完全没必要用一块纹理来专门保存斜体的图形数据,只要用正常文字的纹理在映射时做一次一定角度的偏移就实现了斜体,而且效果不错。 |