原文 2 N9 u% L8 e: K$ w7 |! |9 x
http://blog.chinaunix.net/u/26313/showart_1663543.html ; r9 D5 o6 O2 p f
4 t: W1 y1 e& g, y4 q1 d
内容超多的一课!不过我想精彩的程度也一定不会让大家失望。大家不妨先浏览一下课程里的图片:)。
' Z% d! {* v' P
- b' L( L5 O: k' i本课我们来谈谈如何显示文字。 0 `, w/ v/ n! k# k$ k
OpenGL并没有直接提供显示文字的功能,并且,OpenGL也没有自带专门的字库。因此,要显示文字,就必须依赖操作系统所提供的功能了。 , I: w0 s' |* l/ A" Z k9 d0 t2 `6 h1 O
各种流行的图形操作系统,例如Windows系统和Linux系统,都提供了一些功能,以便能够在OpenGL程序中方便的显示文字。
3 F& d s7 S0 r8 v9 o2 U: V. W最常见的方法就是,我们给出一个字符,给出一个显示列表编号,然后操作系统由把绘制这个字符的OpenGL命令装到指定的显示列表中。当需要绘制字符的时候,我们只需要调用这个显示列表即可。
) z1 @1 i" f0 i7 K5 G不过,Windows系统和Linux系统,产生这个显示列表的方法是不同的(虽然大同小异)。作为我个人,只在Windows系统中编程,没有使用Linux系统的相关经验,所以本课我们仅针对Windows系统。 0 j& B" N+ `7 P8 a: h! b
) G5 `8 g( e' C( ~. lOpenGL版的“Hello, World!”
; v# N3 \ t" ?2 H; c! i3 D写完了本课,我的感受是:显示文字很简单,显示文字很复杂。看似简单的功能,背后却隐藏了深不可测的玄机。
1 h- O4 e: e# w' ~: Z2 H呵呵,别一开始就被吓住了,让我们先从“Hello, World!”开始。 8 E0 i/ a! H! ]" d- ^) B
前面已经说过了,要显示字符,就需要通过操作系统,把绘制字符的动作装到显示列表中,然后我们调用显示列表即可绘制字符。 3 z3 y: Q* a5 q3 x' `: E# S6 ~% C0 I9 Q" w
假如我们要显示的文字全部是ASCII字符,则总共只有0到127这128种可能,因此可以预先把所有的字符分别装到对应的显示列表中,然后在需要时调用这些显示列表。
4 h& W, m! W" l% D! d- B6 J% |9 fWindows系统中,可以使用wglUseFontBitmaps函数来批量的产生显示字符用的显示列表。函数有四个参数: 3 R9 t! y& ^1 G8 G' B
第一个参数是HDC,学过Windows GDI的朋友应该会熟悉这个。如果没有学过,那也没关系,只要知道调用wglGetCurrentDC函数,就可以得到一个HDC了。具体的情况可以看下面的代码。 ( s, m* M, s. j! E: Q
第二个参数表示第一个要产生的字符,因为我们要产生0到127的字符的显示列表,所以这里填0。 7 @( M" a& ]8 N' F" f7 [, P' \' T
第三个参数表示要产生字符的总个数,因为我们要产生0到127的字符的显示列表,总共有128个字符,所以这里填128。
0 R! Q0 k% g; v, h* ?4 m9 Z第四个参数表示第一个字符所对应显示列表的编号。假如这里填1000,则第一个字符的绘制命令将被装到第1000号显示列表,第二个字符的绘制命令将被装到第1001号显示列表,依次类推。我们可以先用glGenLists申请128个连续的显示列表编号,然后把第一个显示列表编号填在这里。
: v* L, E- C9 I7 z还要说明一下,因为wglUseFontBitmaps是Windows系统特有的函数,所以在使用前需要加入头文件:#include <windows.h>。 ) ^7 p" X8 _9 M+ ~) D
现在让我们来看具体的代码: ( {% E% A5 D0 L; Q% `5 ?# |
' i: Y8 j- X/ k5 u4 C9 d) ^
#include <windows.h>
1 P8 s( j! H; a) t8 o( t0 Q3 x: Y) x+ _6 h* T
// ASCII字符总共只有0到127,一共128种字符 " Z6 [; H9 O5 v1 i, z7 V+ C; @
#define MAX_CHAR 128 & Z k# [; h. o; W& ?3 K. B
! d ]+ l0 Z/ S: Y1 {. i/ U5 v
void drawString(const char* str) { $ W; I) f( y' c6 d p! p
static int isFirstCall = 1;
# T) n l) J9 e2 h5 P static GLuint lists; : R7 Y8 R5 l9 g; c7 T1 v" ]
/ b8 J i( H1 Z4 s, D) ~/ X* b | if( isFirstCall ) { // 如果是第一次调用,执行初始化 $ a' z8 F. ]$ u; R
// 为每一个ASCII字符产生一个显示列表 . A7 }5 M" j8 n q# C, p
isFirstCall = 0; ( U( x$ b4 C& d: j
# n$ a5 z+ i- a: \
// 申请MAX_CHAR个连续的显示列表编号 $ L+ ]. N$ k6 c! }( Q0 p
lists = glGenLists(MAX_CHAR); 7 I# Q* n7 z% ^# A' l1 D
3 d7 v6 ?* l! _" R; K3 b% a4 j
// 把每个字符的绘制命令都装到对应的显示列表中
7 C" g/ z+ \- C2 w1 x wglUseFontBitmaps(wglGetCurrentDC(), 0, MAX_CHAR, lists);
% H$ `; n- }4 A5 e- ?% E }
9 M J" ~4 Q9 W // 调用每个字符对应的显示列表,绘制每个字符 . J" `2 [% l. T! a v7 u
for(; *str!='\0'; ++str)
7 A( c E3 X, N9 A% d# W5 W glCallList(lists + *str); ' c0 j- w# |4 f7 G8 U5 {- Q
} ! c7 L, i$ _5 v2 H
, K+ ?6 e& B, T- f7 h
; [( {( [8 K: O! [3 f) q; {! I" i `; Q
显示列表一旦产生就一直存在(除非调用glDeleteLists销毁),所以我们只需要在第一次调用的时候初始化,以后就可以很方便的调用这些显示列表来绘制字符了。 * _3 d, V0 l% i# Q* J
绘制字符的时候,可以先用glColor*等指定颜色,然后用glRasterPos*指定位置,最后调用显示列表来绘制。 0 ]) j" B* t9 I: y
) `" p- u* c4 B* Q# A
void display(void) {
1 `. N$ ]+ D! E! X, t glClear(GL_COLOR_BUFFER_BIT); * r/ \4 }0 ~9 ?0 m/ W
4 z" D+ v" F. ?: s2 f \6 h' \
glColor3f(1.0f, 0.0f, 0.0f); - P- X& f) _, h" M: @
glRasterPos2f(0.0f, 0.0f);
3 b% W# d- J. d5 S# {: {; N, D* u drawString("Hello, World!"); : T. r! \' M$ n& y ?
( W6 u# T7 H X: p' E- a
glutSwapBuffers(); , w/ F+ A7 {* K x2 U i: D9 J
} & e( U# o( _9 Q1 `/ U
. b% x# A& {7 b: F2 I, I8 e
9 `1 z$ f8 D, T3 r- G& V8 u* X5 J) P& i z2 t
效果如图:
) W2 N6 l8 G4 \& {; }- L
# L% H( R+ ^9 _6 D; G$ [% W7 i
! I/ E6 c% s: Y. x指定字体
3 w5 f* U. \( J: ]! ^: U在产生显示列表前,Windows允许选择字体。 ( {4 |& Q+ N- Q3 K% }8 S% Q7 T
我做了一个selectFont函数来实现它,大家可以看看代码。
3 n I. w2 v+ F/ n+ T
0 _7 f' G5 o1 m) x* ]void selectFont(int size, int charset, const char* face) {
. K1 X+ |& J4 H" w9 B; m2 K HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0,
: A0 \+ b+ n/ M, C- ` charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, 4 r% F4 y- D' @
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face); * J; I6 u9 m; r
HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont);
/ @1 d; V4 I# j5 ~# P DeleteObject(hOldFont); ! I( b/ Q" t) v. O: N: r# o
} 9 S9 b2 J5 \% @" O4 b* E1 I% S* X
/ R% c" J8 b9 S; s! ?. Z2 \void display(void) {
* s! Y/ a& C* }6 C$ B selectFont(48, ANSI_CHARSET, "Comic Sans MS");
& ? U! `4 A- B( d+ U G
0 @' m! [% U# Z2 W- H$ j' x glClear(GL_COLOR_BUFFER_BIT);
( O6 }! u# j# P/ m8 ~$ S
, |! t: D1 e- d5 L glColor3f(1.0f, 0.0f, 0.0f);
# c+ f! T2 F' \+ ^$ c3 u glRasterPos2f(0.0f, 0.0f);
5 i8 p+ B$ }% y5 A drawString("Hello, World!"); $ Y q! R! J8 e/ [
2 T* Q5 P' ]; ?' C+ m9 O glutSwapBuffers(); * }' Z! C) v' e) E7 w: }/ E. G
} 2 E( Y9 g0 G$ c2 s# d3 q. A
5 V, G- P: a/ c% l- r! s7 N* @; c
最主要的部分就在于那个参数超多的CreateFont函数,学过Windows GDI的朋友应该不会陌生。没有学过GDI的朋友,有兴趣的话可以自己翻翻MSDN文档。这里我并不准备仔细讲这些参数了,下面的内容还多着呢:( ) r' X L: P& J. T R7 j
如果需要在自己的程序中选择字体的话,把selectFont函数抄下来,在调用glutCreateWindow之后、在调用wglUseFontBitmaps之前使用selectFont函数即可指定字体。函数的三个参数分别表示了字体大小、字符集(英文字体可以用ANSI_CHARSET,简体中文字体可以用GB2312_CHARSET,繁体中文字体可以用CHINESEBIG5_CHARSET,对于中文的Windows系统,也可以直接用DEFAULT_CHARSET表示默认字符集)、字体名称。
) D( W! V4 j# S# N( v7 _9 N- E5 K效果如图:
( j* g" e0 u8 A- g. ]/ t. r 2 ]( w. |4 g+ l0 D/ i
- A8 n+ E0 g3 K* s# R( Q显示中文 % K% q& z# I0 S- X/ b
原则上,显示中文和显示英文并无不同,同样是把要显示的字符做成显示列表,然后进行调用。 5 ~7 `# T- E7 {# k
但是有一个问题,英文字母很少,最多只有几百个,为每个字母创建一个显示列表,没有问题。但是汉字有非常多个,如果每个汉字都产生一个显示列表,这是不切实际的。 : o. s9 p6 \6 L0 T
我们不能在初始化时就为每个字符建立一个显示列表,那就只有在每次绘制字符时创建它了。当我们需要绘制一个字符时,创建对应的显示列表,等绘制完毕后,再将它销毁。
! _! ^9 g( v; j4 v' U5 ]这里还经常涉及到中文乱码的问题,我对这个问题也不甚了解,但是网上流传的版本中,使用了MultiByteToWideChar这个函数的,基本上都没有出现乱码,所以我也准备用这个函数:)
* Q0 c+ b# E k9 r' Q& Z! ?通常我们在C语言里面使用的字符串,如果中英文混合的话,例如“this is 中文字符.”,则英文字符只占用一个字节,而中文字符则占用两个字节。用MultiByteToWideChar函数,可以转化为所有的字符都占两个字节(同时解决了前面所说的乱码问题:))。 - c, j# a% F& u# c+ r
转化的代码如下:
8 }: w! j3 ?7 O8 V% u1 k. T5 P' X% W8 P
// 计算字符的个数 # T7 F7 _2 C5 W/ n1 v+ \) U6 P
// 如果是双字节字符的(比如中文字符),两个字节才算一个字符 1 Q/ G# H+ |8 U3 w, l3 C1 X
// 否则一个字节算一个字符
) C7 B8 x( o/ i& [! O' w+ `len = 0; & b4 P M1 _0 V5 _
for(i=0; str!='\0'; ++i)
; H$ j" W4 m: D. r x Q{ ! P. k x$ u; T0 l w8 a
if( IsDBCSLeadByte(str) )
% _! J) H4 O1 q, ~- c$ _4 E ++i; * l# H* j- o$ }: p
++len; , i2 Z" C" S1 n$ i! `
}
6 `9 z- U/ d0 z/ M$ T
% s4 A6 I t# c! U }3 ?9 X// 将混合字符转化为宽字符 ! u- w7 J! ^/ a% l
wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
* N8 e, V" I4 C$ k$ k3 fMultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len); 5 D% z' D) K. h* Y! [7 `
wstring[len] = L'\0'; 5 v! G8 p+ H; @! U2 F8 k
! a& j3 x7 J/ }2 d// 用完后记得释放内存
- ~2 U2 p; d+ D! g+ F. R, H- b0 W, Ffree(wstring); * m1 H# w( r! a; ^" w8 M3 Z" {
x4 C H% ?) p0 K7 \6 ]. f
" D6 ^6 L/ S/ C" k s% L/ u+ y& p
- \) U2 n1 W& z/ j6 i加上前面所讲到的wglUseFontBitmaps函数,即可显示中文字符了。 8 w% A1 V* g0 s( E0 R5 e
, `0 q& q1 ^/ x5 t+ N: Mvoid drawCNString(const char* str) {
6 }+ w/ I( ^) R) O7 ?+ n3 ? int len, i;
9 O. H+ q' \: x) M; W wchar_t* wstring;
, x" C; v, h2 t+ E# c HDC hDC = wglGetCurrentDC();
% a }' I6 T+ r0 K6 m2 c GLuint list = glGenLists(1);
- b, H; f2 J! S% Y
: h; ?1 R) r9 D7 T // 计算字符的个数
+ ], y" M6 `: F* H7 g // 如果是双字节字符的(比如中文字符),两个字节才算一个字符 ' Y3 v) j: K7 L3 e* o
// 否则一个字节算一个字符 & y% T' V0 }6 }7 `1 v" e$ j( Y& `
len = 0;
) N' O+ n4 B8 {! s& S- g for(i=0; str!='\0'; ++i)
' ]; B( v0 {3 M5 x: f0 y, U- @ { 4 h, F% Z5 s: ?1 l0 N! e8 H
if( IsDBCSLeadByte(str) ) $ k2 U7 e- a& ~: A
++i;
( W6 q7 O& I; ~ F9 c) W ++len; 0 ]; A. o( S& ? S# {8 N
} % [& [4 k+ T J3 x( G; p+ ~2 V
7 c, B6 _, b# I$ ]) X; D% x: ?% @
// 将混合字符转化为宽字符
! i. ] P5 j: ?' ^, J. L9 z4 x/ P wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t));
. Q5 d9 v! x+ L) @) } MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
4 A/ K9 |9 O' j$ P wstring[len] = L'\0';
( G! `; R2 Q1 x
" @! I; _, Q# h8 z' J // 逐个输出字符 / z" l+ P! m9 v G" g* ~8 K
for(i=0; i<len; ++i) `( c( r1 J: i$ G
{ / c" d. J( i/ h8 Y" N
wglUseFontBitmapsW(hDC, wstring, 1, list); . [: x) b% J. R2 m5 x- t- x
glCallList(list); + v/ F/ X8 Q" |5 h3 B
} @9 G6 u0 g$ q
6 Z) \. d+ Z4 p // 回收所有临时资源 1 `! {& J' S5 g, o; j! |9 f" ?, H
free(wstring);
7 n% _2 w) U+ b/ q8 D0 F glDeleteLists(list, 1);
- t# H7 b/ l' ?6 F} - z2 K) {, F4 p
( H# g0 @. k! z3 r2 @
; A+ k5 k, X1 W7 X. h. s) k* Z
& i$ h8 \. {! W O7 X
注意我用了wglUseFontBitmapsW函数,而不是wglUseFontBitmaps。wglUseFontBitmapsW是wglUseFontBitmaps函数的宽字符版本,它认为字符都占两个字节。因为这里使用了MultiByteToWideChar,每个字符其实是占两个字节的,所以应该用wglUseFontBitmapsW。 8 b" h1 ?* k# z, _# t
/ y( n2 }# _5 A! [7 jvoid display(void) { , l. J' u2 ?# k
glClear(GL_COLOR_BUFFER_BIT); 6 C# P6 ~1 O- V1 j2 d
7 ~2 [9 i( |2 g# _5 u! R0 c3 F- s selectFont(48, ANSI_CHARSET, "Comic Sans MS");
) {% O9 A2 }2 e2 U- @* H glColor3f(1.0f, 0.0f, 0.0f); # X; o$ b* C" Z' E7 g& H
glRasterPos2f(-0.7f, 0.4f);
9 J& f" v6 w4 g! ?; ~: q7 @2 U/ i drawString("Hello, World!");
' B5 z/ e. o$ q# M
0 F& [# X' H0 b* u5 x1 u' D# U2 q selectFont(48, GB2312_CHARSET, "楷体_GB2312");
% B3 ?6 ?& o0 u. i% `1 W glColor3f(1.0f, 1.0f, 0.0f);
! b! B2 o8 P8 N+ F" i- Q9 P glRasterPos2f(-0.7f, -0.1f); & @9 p$ I5 h |' G3 u# ~+ i* ?
drawCNString("当代的中国汉字"); 0 z/ X+ Y2 n5 J4 p5 K) t
+ {, V; |+ H$ s selectFont(48, DEFAULT_CHARSET, "华文仿宋"); # l/ b7 \9 M( Y. ]
glColor3f(0.0f, 1.0f, 0.0f); , @# F5 \% C5 y. a
glRasterPos2f(-0.7f, -0.6f); . g H1 |. {1 @; @1 s5 ^7 Z8 [
drawCNString("傳統的中國漢字"); ; w7 h) v4 Q' l' ~6 m
* m% i2 J7 r4 l5 h0 }6 S& @
glutSwapBuffers();
7 G4 i I1 t: J0 Q- W} : ^: z$ c9 j+ [
) t+ S/ ^2 ~0 l- y; j4 I) v/ ~! v1 ]9 g. o/ l3 |1 q
效果如图:
8 }+ `9 S5 r. x7 ^; _4 t+ E |