原文
6 D Z7 w9 ?% Q* Q7 C( Lhttp://blog.chinaunix.net/u/26313/showart_1663543.html ! S R+ Y% l9 Y) s
) g5 G3 i0 N. f/ R5 \内容超多的一课!不过我想精彩的程度也一定不会让大家失望。大家不妨先浏览一下课程里的图片:)。
( ? e4 X9 b! X* e" n8 C/ V/ O- |4 G9 ~' \& T: `& o3 S
本课我们来谈谈如何显示文字。 4 A9 l n/ ^ O
OpenGL并没有直接提供显示文字的功能,并且,OpenGL也没有自带专门的字库。因此,要显示文字,就必须依赖操作系统所提供的功能了。
4 F# t+ I4 k b0 r/ _6 x: b. A4 m8 C0 h各种流行的图形操作系统,例如Windows系统和Linux系统,都提供了一些功能,以便能够在OpenGL程序中方便的显示文字。
9 W5 |( i. m: C1 p' G# k0 c+ U最常见的方法就是,我们给出一个字符,给出一个显示列表编号,然后操作系统由把绘制这个字符的OpenGL命令装到指定的显示列表中。当需要绘制字符的时候,我们只需要调用这个显示列表即可。 " W& t+ K9 S# v7 x' b
不过,Windows系统和Linux系统,产生这个显示列表的方法是不同的(虽然大同小异)。作为我个人,只在Windows系统中编程,没有使用Linux系统的相关经验,所以本课我们仅针对Windows系统。 ; [6 ?, D- @ w) [
! V- w3 I- @0 z( S+ q" uOpenGL版的“Hello, World!” $ [. W7 e$ l0 n% O% Y
写完了本课,我的感受是:显示文字很简单,显示文字很复杂。看似简单的功能,背后却隐藏了深不可测的玄机。
. ?) U% w( u" [" r; [+ O7 m9 t9 H5 @5 p呵呵,别一开始就被吓住了,让我们先从“Hello, World!”开始。 9 b6 ?& x% t. L$ V( R6 p
前面已经说过了,要显示字符,就需要通过操作系统,把绘制字符的动作装到显示列表中,然后我们调用显示列表即可绘制字符。
, d6 D% \5 ^' _/ T/ a+ Z# h假如我们要显示的文字全部是ASCII字符,则总共只有0到127这128种可能,因此可以预先把所有的字符分别装到对应的显示列表中,然后在需要时调用这些显示列表。
3 \2 j e, h( _/ i( A7 ZWindows系统中,可以使用wglUseFontBitmaps函数来批量的产生显示字符用的显示列表。函数有四个参数: 9 `/ W4 u' G' o2 }, w; u( I7 t) a
第一个参数是HDC,学过Windows GDI的朋友应该会熟悉这个。如果没有学过,那也没关系,只要知道调用wglGetCurrentDC函数,就可以得到一个HDC了。具体的情况可以看下面的代码。
7 X8 t* R! H4 Z8 I: b第二个参数表示第一个要产生的字符,因为我们要产生0到127的字符的显示列表,所以这里填0。
, d+ d0 {/ [7 B% V第三个参数表示要产生字符的总个数,因为我们要产生0到127的字符的显示列表,总共有128个字符,所以这里填128。 ( V- y# D# _! A/ R4 P) k$ Y
第四个参数表示第一个字符所对应显示列表的编号。假如这里填1000,则第一个字符的绘制命令将被装到第1000号显示列表,第二个字符的绘制命令将被装到第1001号显示列表,依次类推。我们可以先用glGenLists申请128个连续的显示列表编号,然后把第一个显示列表编号填在这里。 # s9 w5 [3 f, n" K G
还要说明一下,因为wglUseFontBitmaps是Windows系统特有的函数,所以在使用前需要加入头文件:#include <windows.h>。 2 n Z) M2 J( O @' v( Y/ t6 y$ j
现在让我们来看具体的代码:
O* ^7 a, _9 H7 B3 }9 W2 r5 W/ {! J6 i5 T3 d) V( g: O
#include <windows.h>
; {0 g9 ?! l- U8 y8 q n
0 E( v" K, I4 W4 U3 D; R# Z// ASCII字符总共只有0到127,一共128种字符
/ J9 Y, Y3 c- {! B#define MAX_CHAR 128
- R* X) k" s1 d1 i( l" L% A% z$ M
& E' ]4 c9 O, d) T+ B% _void drawString(const char* str) { 6 d' U' j+ r+ n1 e9 y( `$ L9 q
static int isFirstCall = 1;
: h, `* h3 \4 i! y E5 w' ^* n) O; C static GLuint lists;
8 R) J3 X' P' H0 C
7 D. |$ L; ?- M J* Q, s, g if( isFirstCall ) { // 如果是第一次调用,执行初始化
' k4 H' w$ Q7 f // 为每一个ASCII字符产生一个显示列表
; z; B2 X# e3 c! W: @8 l# t9 O7 O* W) P* B isFirstCall = 0;
; D% k7 N; l5 R- ]* P
: g& `- U& ?" f% R; Q; _4 m O // 申请MAX_CHAR个连续的显示列表编号 - z8 R; n& I$ \& f6 `6 d" |
lists = glGenLists(MAX_CHAR); , v, n& N. n8 \" L0 D! d
: K* X8 o1 X7 e6 {) | H2 z // 把每个字符的绘制命令都装到对应的显示列表中 ; e1 z9 U- T: v! y& T0 ~1 p/ K1 H
wglUseFontBitmaps(wglGetCurrentDC(), 0, MAX_CHAR, lists);
) K. [0 O$ M7 W3 |( J- _9 a } 3 s6 [5 S: N$ m' a4 P/ }8 {
// 调用每个字符对应的显示列表,绘制每个字符 ( \" b8 x: S) ]" M! f5 Y
for(; *str!='\0'; ++str) - I% d2 _$ T" B# c0 x* o4 Y9 S9 K
glCallList(lists + *str);
2 V+ K- }6 Q7 c5 P, {" f! h}
5 @) t. _( e) s# [/ x! Z- |$ K2 \$ A. _! _8 v
& y0 z3 O" Y: C% s% |* L; l+ u$ Q% m3 p$ H; M5 b/ Z
显示列表一旦产生就一直存在(除非调用glDeleteLists销毁),所以我们只需要在第一次调用的时候初始化,以后就可以很方便的调用这些显示列表来绘制字符了。
* b7 @/ c% l8 v+ a- h/ x; _绘制字符的时候,可以先用glColor*等指定颜色,然后用glRasterPos*指定位置,最后调用显示列表来绘制。 3 x! _ |. d; O5 s/ ^# B
7 C1 d/ w: ^( T2 |
void display(void) {
+ ~. s2 p: a; {' z' l; o glClear(GL_COLOR_BUFFER_BIT); 3 I3 t5 k, B1 Q1 l a, F: ]
. M5 W0 T& i* C0 S" g: _' W: e! { glColor3f(1.0f, 0.0f, 0.0f); " c/ n8 z: G, e2 p
glRasterPos2f(0.0f, 0.0f);
3 Q. w; K. P: I; l4 E drawString("Hello, World!");
G3 {& L/ _" E& z
) [# f: ^7 Y$ U- l8 A, s glutSwapBuffers(); % ~$ g7 e5 |; q
}
% l! O* F/ ~& U7 ~- N$ d4 M' ^" S, k6 [, p" ?8 q
& s6 X0 E$ }+ y7 F& Y8 H
8 _. H1 K0 d+ R. V1 D9 D3 P效果如图:
1 q7 X$ T& s9 s
5 g F/ x+ T% H- v4 L- w" F6 `$ ^" y9 Q( u* K7 _
指定字体 % _( P7 C `( u
在产生显示列表前,Windows允许选择字体。
+ w8 X* r! n/ K: ]) d+ @我做了一个selectFont函数来实现它,大家可以看看代码。
H, i+ M, e& N2 S3 Q0 B
a' Z5 [- r9 Z% ovoid selectFont(int size, int charset, const char* face) { + H3 U; P% D! @& _. {' J
HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0,
1 W" L& Q4 s C; w; u charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ( J! J2 o Z+ X! f9 o
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face); ! A. s( M2 i! p3 p' r4 S
HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont); s% ?# Y" Q1 Y
DeleteObject(hOldFont);
8 c5 q6 }7 }9 L* u* V} ! ~" k5 v- W: p4 H$ n% c
k6 ~+ o9 ], p1 }$ {void display(void) { 7 O# Z3 } c; i1 i& }; p
selectFont(48, ANSI_CHARSET, "Comic Sans MS");
$ i5 @. K6 ~3 ` O) R# W! ]: j# h( @, k2 _4 r4 i
glClear(GL_COLOR_BUFFER_BIT);
3 {. d0 i5 X; h# b& |
3 z* X3 u' ~+ J* l( c- Y* e) j glColor3f(1.0f, 0.0f, 0.0f); ) ]3 E4 ~9 F7 p2 i3 B) p! r
glRasterPos2f(0.0f, 0.0f);
# _. p; s( @! l$ F0 b drawString("Hello, World!");
4 {0 D2 o5 _0 u6 k8 R, N4 l8 L; {" R6 ]* Y0 h H9 ~/ }" F
glutSwapBuffers(); 7 l% W! P y. `& n& x: ]0 V
} + z; U' O" v- z
1 Z' `' m* l1 j) h8 p# j+ D
7 h- Z! e. J0 R1 u5 h6 n最主要的部分就在于那个参数超多的CreateFont函数,学过Windows GDI的朋友应该不会陌生。没有学过GDI的朋友,有兴趣的话可以自己翻翻MSDN文档。这里我并不准备仔细讲这些参数了,下面的内容还多着呢:( ; t7 f2 I" x. N: ^' v6 ?. j
如果需要在自己的程序中选择字体的话,把selectFont函数抄下来,在调用glutCreateWindow之后、在调用wglUseFontBitmaps之前使用selectFont函数即可指定字体。函数的三个参数分别表示了字体大小、字符集(英文字体可以用ANSI_CHARSET,简体中文字体可以用GB2312_CHARSET,繁体中文字体可以用CHINESEBIG5_CHARSET,对于中文的Windows系统,也可以直接用DEFAULT_CHARSET表示默认字符集)、字体名称。 & {1 p5 g9 O; m1 `" F# l
效果如图:
?5 a0 w! R) a- Q4 ^& L / X* h' E" T7 W4 m* e& K
% [( k4 d( a; t+ Z( W/ m
显示中文
8 M- G- r: `8 h% N9 T, Z原则上,显示中文和显示英文并无不同,同样是把要显示的字符做成显示列表,然后进行调用。 2 l8 P1 F* G5 Z& U/ X% E P0 E" R
但是有一个问题,英文字母很少,最多只有几百个,为每个字母创建一个显示列表,没有问题。但是汉字有非常多个,如果每个汉字都产生一个显示列表,这是不切实际的。 9 }, l* _9 l9 Q. ~. M" p* h* R
我们不能在初始化时就为每个字符建立一个显示列表,那就只有在每次绘制字符时创建它了。当我们需要绘制一个字符时,创建对应的显示列表,等绘制完毕后,再将它销毁。 / k' l) W& q7 q# {7 B
这里还经常涉及到中文乱码的问题,我对这个问题也不甚了解,但是网上流传的版本中,使用了MultiByteToWideChar这个函数的,基本上都没有出现乱码,所以我也准备用这个函数:) 7 A5 H# w* k/ m2 N
通常我们在C语言里面使用的字符串,如果中英文混合的话,例如“this is 中文字符.”,则英文字符只占用一个字节,而中文字符则占用两个字节。用MultiByteToWideChar函数,可以转化为所有的字符都占两个字节(同时解决了前面所说的乱码问题:))。 ( D; d# B) x$ _& K9 l8 d4 i
转化的代码如下:
1 {. t4 G3 c2 d; \3 v4 L) Y6 [8 O. Z9 o" i) N6 }
// 计算字符的个数
6 q- o# D3 p. }1 `& k// 如果是双字节字符的(比如中文字符),两个字节才算一个字符 ! V& r% H# L( T, L6 w2 q1 R
// 否则一个字节算一个字符 ' E% R$ g g; ` {" ~' N
len = 0; 7 E3 [3 D) d4 w' \; f
for(i=0; str!='\0'; ++i) 2 O6 s! _4 k1 v, ?. W
{ ; S7 L3 f; x' U- @9 I
if( IsDBCSLeadByte(str) ) ( u8 w4 k, I8 J2 t- F3 i4 V
++i; # @2 Y6 G. y8 u* g( F) J% \4 s# L: J _8 C
++len;
. r% x6 O' H% ^7 }0 f2 e, [}
0 U. [; I! e2 p% @* D9 S$ D8 I | D+ q4 s% g5 s+ s
// 将混合字符转化为宽字符 # d, A# w3 O; d9 f& d
wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
, b" N9 _+ v' n8 ~) @, q% dMultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
3 T* y( l9 p# {' S$ X6 Mwstring[len] = L'\0'; 4 {# K& @. {) A1 }6 j! O+ {, a
8 G% t7 X5 k1 r4 h& L; B: m! r
// 用完后记得释放内存
8 n+ x3 Q' r5 `+ T `free(wstring); % c( z/ O1 ^0 D% k6 S b0 W: t; B
+ d( i: s6 o* M9 Z. ]8 ^- C$ Z
3 v- x7 a& t, P5 i. V3 N5 D! b R3 ?4 h8 ~( @1 J, x
加上前面所讲到的wglUseFontBitmaps函数,即可显示中文字符了。 3 M" B! Z- L7 O" j# W" T
/ S( d" e. D: ?void drawCNString(const char* str) {
6 L( S" T' @1 s- k1 o B. W2 ] int len, i; * }. B& E) u6 R, e- s& n$ ]
wchar_t* wstring; " d n7 T2 K& A# e# E3 X- p
HDC hDC = wglGetCurrentDC(); i# w5 g o6 d1 l' \, p
GLuint list = glGenLists(1);
; y/ Z- t! t! N9 w
" ^5 Y; s/ b$ I! p5 ~: f: e% F( D // 计算字符的个数 : g! K# n; H; k" E' _6 R9 p: H5 G
// 如果是双字节字符的(比如中文字符),两个字节才算一个字符
6 U" h: ? o& D- N; J // 否则一个字节算一个字符
! ^$ S- _# X& ], b, U- T: c7 [: Q' m len = 0;
3 {& e! ?) x* w for(i=0; str!='\0'; ++i)
* ?/ p: W0 s) ]; b! x {
- {! W& R+ J' U* n8 T8 u5 ]( l& L s if( IsDBCSLeadByte(str) ) : `9 Z Y. J6 F; U
++i; 4 ^& h8 G$ j+ Z n# h
++len;
% _0 e' w$ f( ~1 ?1 U7 e9 Q- ^! Q3 J }
K' v2 Q& P2 l1 B
& {7 T! M" S" ]# p2 j8 a, m) T4 a6 | // 将混合字符转化为宽字符
2 S' |7 I7 g9 Y( c wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
- c" n' u. ]6 i2 P MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len); 4 w2 `2 c" @& q0 j7 e& ?3 Z
wstring[len] = L'\0';
* D" W" }6 c3 B2 ]1 R( s
6 i" \. m0 D5 p3 _7 ]0 U" J \/ ]7 o/ T // 逐个输出字符
6 [9 h* |( ~5 l1 _' j for(i=0; i<len; ++i)
/ A3 z9 T% O# D8 S6 |9 }7 D8 M { - s) }& ]$ S7 H. r
wglUseFontBitmapsW(hDC, wstring, 1, list); ) |6 n+ h6 [3 p0 v4 [8 ~9 ?
glCallList(list);
% o% X6 H v" }2 n: _ } ) C# j8 n' \! h1 n- J
$ h Q: v" J/ a8 y // 回收所有临时资源 ; {* Z- Q4 p3 m- O+ M3 T! B
free(wstring); 4 N4 N7 a. G6 I) L4 K
glDeleteLists(list, 1);
" p0 ]# l+ T7 z3 E+ i, W}
# U7 K+ v9 D2 `/ a% t1 V: F' G
6 A& x, N: y. y2 d) e( Z
9 G6 y) Z& _8 b
$ P$ @/ P' G' m- J+ J) m5 e注意我用了wglUseFontBitmapsW函数,而不是wglUseFontBitmaps。wglUseFontBitmapsW是wglUseFontBitmaps函数的宽字符版本,它认为字符都占两个字节。因为这里使用了MultiByteToWideChar,每个字符其实是占两个字节的,所以应该用wglUseFontBitmapsW。
. V" Q. ~, c& R# r
% I* j2 w) r8 K" @7 O, avoid display(void) {
) f0 ~6 V8 y7 B X: H glClear(GL_COLOR_BUFFER_BIT); ( H& o& ^. P7 |6 s; J( W4 U
' h& h' T/ _* g
selectFont(48, ANSI_CHARSET, "Comic Sans MS");
/ e+ V8 k' G7 q7 N% j glColor3f(1.0f, 0.0f, 0.0f); ' k2 ?- z' a7 n0 p; P9 `1 t/ X
glRasterPos2f(-0.7f, 0.4f);
+ y; d$ O1 K6 R [9 t6 m drawString("Hello, World!"); . X0 V, ?0 ~ B3 E2 d, _' _
+ m% q2 R c, x, C w+ h+ b selectFont(48, GB2312_CHARSET, "楷体_GB2312");
4 d8 P6 u/ c4 x/ x glColor3f(1.0f, 1.0f, 0.0f); * z: @1 c0 D. y6 h( }- r
glRasterPos2f(-0.7f, -0.1f);
- f* l8 n3 D. r8 N4 x drawCNString("当代的中国汉字");
& n; D. r6 n, j' C' ~" ~4 R
5 j* `7 A- H$ i8 m selectFont(48, DEFAULT_CHARSET, "华文仿宋");
( A2 d, G7 _) T) K4 Y glColor3f(0.0f, 1.0f, 0.0f);
1 a8 n1 Y; B# T* H- e+ u8 t. o glRasterPos2f(-0.7f, -0.6f);
+ p# k+ Z2 {# o K0 e drawCNString("傳統的中國漢字"); t# k9 K8 S6 X7 U3 v9 T0 \
8 W+ k9 n3 d+ I9 Y4 u( ]
glutSwapBuffers(); ! T& g/ }9 D/ B Q) m
} # E0 ]( q- B+ N- R1 G" v# d
5 |4 K% W) U ~0 q+ ]
6 U5 O! ]* V$ W; j5 ~' G j效果如图:6 x% p2 S) s! _$ z4 t& I' S7 P
|