原文 5 ~! ~1 N( i9 p3 R0 m+ e5 `& Z
http://blog.chinaunix.net/u/26313/showart_1663543.html
d) B0 `2 n/ K3 d& g2 Y; ^* {+ R2 I
内容超多的一课!不过我想精彩的程度也一定不会让大家失望。大家不妨先浏览一下课程里的图片:)。
& Q: ^- m8 J- K& \5 D% }3 I6 o9 a! F ?' G) b
本课我们来谈谈如何显示文字。
1 f* q) ]6 n1 W& B) \, P* N$ e1 hOpenGL并没有直接提供显示文字的功能,并且,OpenGL也没有自带专门的字库。因此,要显示文字,就必须依赖操作系统所提供的功能了。
' X6 ?: C/ r3 H各种流行的图形操作系统,例如Windows系统和Linux系统,都提供了一些功能,以便能够在OpenGL程序中方便的显示文字。
; @. f3 H% {4 \5 q最常见的方法就是,我们给出一个字符,给出一个显示列表编号,然后操作系统由把绘制这个字符的OpenGL命令装到指定的显示列表中。当需要绘制字符的时候,我们只需要调用这个显示列表即可。 " \+ j" ?% n- ]4 V8 ~) o
不过,Windows系统和Linux系统,产生这个显示列表的方法是不同的(虽然大同小异)。作为我个人,只在Windows系统中编程,没有使用Linux系统的相关经验,所以本课我们仅针对Windows系统。
( L0 P$ R8 X- i0 c
4 T# f! B& t$ ROpenGL版的“Hello, World!”
$ f3 x) c- A$ m" S1 d8 `7 V写完了本课,我的感受是:显示文字很简单,显示文字很复杂。看似简单的功能,背后却隐藏了深不可测的玄机。 . N, j& |$ V! b4 i) ~$ Q/ k
呵呵,别一开始就被吓住了,让我们先从“Hello, World!”开始。 ; a) j7 T6 n- `- g7 m2 V" U" |
前面已经说过了,要显示字符,就需要通过操作系统,把绘制字符的动作装到显示列表中,然后我们调用显示列表即可绘制字符。 $ Q7 {7 G1 [1 f" ^" x% G: q
假如我们要显示的文字全部是ASCII字符,则总共只有0到127这128种可能,因此可以预先把所有的字符分别装到对应的显示列表中,然后在需要时调用这些显示列表。 + S* {+ ?" E; f3 n8 _1 e2 o
Windows系统中,可以使用wglUseFontBitmaps函数来批量的产生显示字符用的显示列表。函数有四个参数: 0 T7 N, I y% e2 X6 a+ W
第一个参数是HDC,学过Windows GDI的朋友应该会熟悉这个。如果没有学过,那也没关系,只要知道调用wglGetCurrentDC函数,就可以得到一个HDC了。具体的情况可以看下面的代码。 + X w9 z8 x1 D, _3 G+ _
第二个参数表示第一个要产生的字符,因为我们要产生0到127的字符的显示列表,所以这里填0。 , p: Q/ L/ H* r6 U& }5 m
第三个参数表示要产生字符的总个数,因为我们要产生0到127的字符的显示列表,总共有128个字符,所以这里填128。 - W1 s* y4 y( _- s
第四个参数表示第一个字符所对应显示列表的编号。假如这里填1000,则第一个字符的绘制命令将被装到第1000号显示列表,第二个字符的绘制命令将被装到第1001号显示列表,依次类推。我们可以先用glGenLists申请128个连续的显示列表编号,然后把第一个显示列表编号填在这里。
z4 {& d; Z! L* `2 }% x `还要说明一下,因为wglUseFontBitmaps是Windows系统特有的函数,所以在使用前需要加入头文件:#include <windows.h>。
$ [4 X( d, ?) t' q( ]& @& O' ~现在让我们来看具体的代码:
4 ]7 r* W% j- H. I0 U2 U6 L, L3 E5 u' X+ c3 b$ c
#include <windows.h>
5 p; d6 B4 u1 K# b' B$ t, z4 E. a9 B8 C# S$ M* V" N% r
// ASCII字符总共只有0到127,一共128种字符 - D' x) j$ ?, }+ t" N# l4 K+ W
#define MAX_CHAR 128
+ i& o M( t: X- v
0 ~; [3 j3 a7 o$ e6 a- rvoid drawString(const char* str) {
3 e( g7 a$ L" O1 ^* d static int isFirstCall = 1; ) o' D$ X4 C% }% z" v% ?
static GLuint lists; " a6 V2 q7 o" G* i( ?3 U
5 u& N& K: @5 w4 l! G0 i! J: l% J if( isFirstCall ) { // 如果是第一次调用,执行初始化
- B3 m0 o" @. `( M4 j# } // 为每一个ASCII字符产生一个显示列表
' A# u6 {' X1 d7 Q, n) T3 I isFirstCall = 0; 7 y7 `5 C' X: s/ I4 x3 c! K; B) j
$ C3 u4 D. s* o ~1 a
// 申请MAX_CHAR个连续的显示列表编号 6 t$ _& w; {- k7 c* M
lists = glGenLists(MAX_CHAR); * t. f. D G8 X, M1 h7 {
+ D7 W1 E. }% b5 i' T/ C2 q
// 把每个字符的绘制命令都装到对应的显示列表中
, w B/ b7 ~5 a' x wglUseFontBitmaps(wglGetCurrentDC(), 0, MAX_CHAR, lists);
3 G: O1 X$ j/ c- A% t4 ?$ R }
! v) d0 Q, B2 U7 { // 调用每个字符对应的显示列表,绘制每个字符
* u# T5 ^5 \! Z2 c+ Q) G for(; *str!='\0'; ++str) ! ?- t$ w% h% o# S
glCallList(lists + *str); ) Y0 n8 r! u! g8 G: F3 v! w7 a
} ( s7 i& t% q8 q. o* ~9 p: ?( A" V. D
9 Q: O, W+ I$ X1 S7 q+ B G# q; N" R, ?+ n
5 _. }7 a, k; u. q. _7 u1 y6 c; }% o
显示列表一旦产生就一直存在(除非调用glDeleteLists销毁),所以我们只需要在第一次调用的时候初始化,以后就可以很方便的调用这些显示列表来绘制字符了。
- \8 P" {+ }3 U绘制字符的时候,可以先用glColor*等指定颜色,然后用glRasterPos*指定位置,最后调用显示列表来绘制。 : n/ p' ^$ [" P' p% A
$ z7 N' i p. j( j# w% f* X5 m! \
void display(void) {
/ ^3 l- y; e' j glClear(GL_COLOR_BUFFER_BIT); ' E& u. G+ m- l. v" X! b
4 o1 q3 U; K) T5 X+ F* g4 C glColor3f(1.0f, 0.0f, 0.0f); h1 \. x5 d8 t [) @
glRasterPos2f(0.0f, 0.0f);
/ m: i4 a% i: l) W$ J drawString("Hello, World!");
% a/ w3 @* L; @: |; `7 `. T" e( v
( h( A: s3 o; V3 y- `- C glutSwapBuffers();
i8 f$ s2 w2 P$ K* h. ]} , u5 ?6 i1 {$ S. T" \! w: s" _
( v/ t3 w" R7 c4 C( M3 U! z
9 ^! W+ k6 E" d0 E+ G/ u( P8 I' i7 ^! p, F/ F4 e
效果如图: , p3 X1 _4 X. F8 Y; R9 w
{4 I! I7 e% j3 Q5 s
- R ^$ b1 b" N5 A2 f* Z指定字体
7 ]9 o. y9 |0 s5 P2 \在产生显示列表前,Windows允许选择字体。 D0 @1 ^2 @5 S: o2 q5 U4 r: W
我做了一个selectFont函数来实现它,大家可以看看代码。
7 S% S' A( |- A) g- ]- f
& p' C" q+ | g4 ~ |void selectFont(int size, int charset, const char* face) { * }! _4 g' ?" R. z0 T: g( i
HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0,
8 j; n9 X. Q1 @7 ]' ~8 M charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, " c7 `2 A6 s8 O' O8 e
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face); ! H2 K6 X; Z7 `4 }4 S- j
HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont);
" \- o B- F' }! M# m% ]: c DeleteObject(hOldFont);
" M/ c$ q- ^ Y1 h8 T h" y} 8 h# `* `4 q$ J& ^# i9 o& U4 h
0 I$ X) Q N$ ~: Z; n j; Rvoid display(void) {
" d1 W+ p; n5 C# b) V+ E' b selectFont(48, ANSI_CHARSET, "Comic Sans MS"); ; N. ~* g2 i9 C" h8 B1 x: C
+ l, G1 O9 X" x: n# G' P! G
glClear(GL_COLOR_BUFFER_BIT); 5 n, [2 R# h W/ E
& X, d1 n4 y% m! X
glColor3f(1.0f, 0.0f, 0.0f);
8 D. a6 _- B; X, g0 o6 M glRasterPos2f(0.0f, 0.0f); 0 {3 s7 P0 d5 @$ U, X i
drawString("Hello, World!"); % D' |* S5 Z3 J% W+ S
8 c! M7 j- f+ ]$ E. Z
glutSwapBuffers(); $ S9 z6 j F6 ]) j% r
}
5 j& O6 W! M# ?" N! L4 }5 D
# c0 z3 @3 b/ S# f J- o8 c, W, }9 [. t& e$ z' i! g0 \9 d& O
最主要的部分就在于那个参数超多的CreateFont函数,学过Windows GDI的朋友应该不会陌生。没有学过GDI的朋友,有兴趣的话可以自己翻翻MSDN文档。这里我并不准备仔细讲这些参数了,下面的内容还多着呢:(
1 R1 B& A3 R/ I \1 b如果需要在自己的程序中选择字体的话,把selectFont函数抄下来,在调用glutCreateWindow之后、在调用wglUseFontBitmaps之前使用selectFont函数即可指定字体。函数的三个参数分别表示了字体大小、字符集(英文字体可以用ANSI_CHARSET,简体中文字体可以用GB2312_CHARSET,繁体中文字体可以用CHINESEBIG5_CHARSET,对于中文的Windows系统,也可以直接用DEFAULT_CHARSET表示默认字符集)、字体名称。
u3 O' v u- k$ U6 l效果如图:
7 S- r/ G. S) ?
- F* d( c: `$ a2 l9 X5 J
6 K2 m) f" }6 n# ?) }; F+ C* c显示中文 - x0 r4 [; n- T/ v2 h7 v; b: `
原则上,显示中文和显示英文并无不同,同样是把要显示的字符做成显示列表,然后进行调用。
- o0 J" m5 A O2 p( l但是有一个问题,英文字母很少,最多只有几百个,为每个字母创建一个显示列表,没有问题。但是汉字有非常多个,如果每个汉字都产生一个显示列表,这是不切实际的。 + _$ E$ b6 J) n
我们不能在初始化时就为每个字符建立一个显示列表,那就只有在每次绘制字符时创建它了。当我们需要绘制一个字符时,创建对应的显示列表,等绘制完毕后,再将它销毁。 3 Q6 e6 Q( v% ^! i( a4 U1 ?
这里还经常涉及到中文乱码的问题,我对这个问题也不甚了解,但是网上流传的版本中,使用了MultiByteToWideChar这个函数的,基本上都没有出现乱码,所以我也准备用这个函数:)
0 h* ]3 \& s, S, s* e0 }/ d; e通常我们在C语言里面使用的字符串,如果中英文混合的话,例如“this is 中文字符.”,则英文字符只占用一个字节,而中文字符则占用两个字节。用MultiByteToWideChar函数,可以转化为所有的字符都占两个字节(同时解决了前面所说的乱码问题:))。
& B8 v! W. l/ D转化的代码如下: & j/ u( D' L# z2 p
0 O" v+ m$ |' Q$ F* n' ^4 H
// 计算字符的个数
1 w/ T6 x5 q$ [* h6 C9 V6 W// 如果是双字节字符的(比如中文字符),两个字节才算一个字符
* s1 D( T5 R% ~# A// 否则一个字节算一个字符
. J7 p" X0 P$ @0 j( @, C$ alen = 0; ; i* t0 ~, {* b# z" E
for(i=0; str!='\0'; ++i)
: F! m( r/ Q, J. |{ & Y \- @; L g5 ?: v0 P
if( IsDBCSLeadByte(str) )
) |5 h0 [8 d% I" _+ Y9 \; Q ++i;
+ A" p% r. ]) F6 p6 f/ j/ }( N$ ~ ++len; , y$ C. g, k" g; U: d* u
}
4 }& L& e! s* T% e' Q( i8 D' _# Z( t# }
// 将混合字符转化为宽字符 : L7 W5 s. Y- U( }& T9 C
wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t)); ( L0 ^3 A C, N) w( |: L' j1 E4 d! Z
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
; a2 p/ ?3 C1 c( ~wstring[len] = L'\0'; * A- v# q; x( A4 S2 u
K+ p# X: E8 q' M. w// 用完后记得释放内存
; b8 P0 q9 v# k x7 {- A+ E6 bfree(wstring); 0 O9 ]- \7 D c/ ]
+ L2 p; ]- K& e% C. X9 i2 \4 t/ J6 i) L
$ ~" z7 Z) A% C加上前面所讲到的wglUseFontBitmaps函数,即可显示中文字符了。
& g. K/ N: K& i- i* g% O5 `* o
1 y$ n- |3 d9 J% h- Lvoid drawCNString(const char* str) {
- V7 f* _* `: n0 x6 \+ V a6 i int len, i;
/ |! p- P" Q, D+ @- m; h wchar_t* wstring; $ _( F- U+ n' c2 P* c( E# J# C
HDC hDC = wglGetCurrentDC(); 6 K0 A2 B1 C" l
GLuint list = glGenLists(1); ! O+ {- @& \$ n1 z6 n# c
7 H+ ]& X* H4 y9 P$ p; R: b8 n // 计算字符的个数
& {6 @/ V5 `# E. V // 如果是双字节字符的(比如中文字符),两个字节才算一个字符
( {4 K" {, p: ^ // 否则一个字节算一个字符
- @( z8 V; E: T- m; c len = 0;
/ G; K) E( x: t! P/ l# U for(i=0; str!='\0'; ++i) . X$ A3 Q3 L5 X# L1 O6 Z0 F7 j4 A
{
% T- [; h0 K: K% W7 d3 o% L if( IsDBCSLeadByte(str) ) 2 i6 N* J2 k3 R( z' V3 T# L
++i; 9 N0 M/ `! x+ T3 ^5 k% E
++len; u- I' q% e; z) D+ Q. [% m6 z& W) r
}
% j; ]* F% z9 [% |$ C
% | O" f' o; _" } // 将混合字符转化为宽字符
u h1 o' j3 k0 g3 |5 ? wstring = (wchar_t*)malloc((len+1) * sizeof(wchar_t)); 1 ^* ]* }2 ]+ e0 H+ E
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len); 1 w; z0 m' {$ r
wstring[len] = L'\0'; ; M/ w( `% C5 G" Y. p
5 O2 V0 h) f I8 W5 J
// 逐个输出字符 2 c7 J% @- v" y. b
for(i=0; i<len; ++i) # H5 a; Y7 z/ Q' ?. W9 O/ L
{ $ i/ `( ?6 P( T R1 z
wglUseFontBitmapsW(hDC, wstring, 1, list); ' Z5 g6 B6 ]: N% {" V2 `
glCallList(list);
* N7 M9 j# F ^5 R' p }
" m6 V O$ }; L7 r" M
( G: k0 C* o( W, @ // 回收所有临时资源
7 H; V5 W5 E1 [* Z free(wstring);
% _, ]) n) w. S5 }; F0 h glDeleteLists(list, 1);
' m7 S3 p6 T+ j# E( r; C} 7 l4 n% C% @$ ^4 e1 J
. P9 v) ?& g- O, W3 Z
* {2 k# R! X0 j& P+ H2 \0 C! l
n Y0 o% T" F+ T6 n注意我用了wglUseFontBitmapsW函数,而不是wglUseFontBitmaps。wglUseFontBitmapsW是wglUseFontBitmaps函数的宽字符版本,它认为字符都占两个字节。因为这里使用了MultiByteToWideChar,每个字符其实是占两个字节的,所以应该用wglUseFontBitmapsW。 3 H; c7 p9 S8 O H+ ?6 O* H
$ d8 {: m: P' c1 E, ], l2 r( T
void display(void) { 9 f, k4 Z1 T$ ]$ O2 \
glClear(GL_COLOR_BUFFER_BIT); & M7 q$ Z; Q+ `5 v9 z
7 c# n6 @( e( U' c
selectFont(48, ANSI_CHARSET, "Comic Sans MS");
' v0 m0 _) Q! {; E9 X glColor3f(1.0f, 0.0f, 0.0f);
7 Y( ^% i9 @) H0 f glRasterPos2f(-0.7f, 0.4f); 5 C6 F# o+ H' G! Y* o/ H) {& |! Q
drawString("Hello, World!");
0 W0 z: a. S, [- t) b. ~
; T1 F7 I. p( _ selectFont(48, GB2312_CHARSET, "楷体_GB2312"); ' V- x! n- e( `0 X+ _
glColor3f(1.0f, 1.0f, 0.0f); ! S" t3 i3 P# I: N( c6 E
glRasterPos2f(-0.7f, -0.1f); + ~9 O/ g; T3 a* N5 T- F! J
drawCNString("当代的中国汉字"); ' b8 U7 u3 {# M& J3 w
; h* Q" W9 G5 I2 u# C ?3 G) j selectFont(48, DEFAULT_CHARSET, "华文仿宋"); 2 u2 q; Q: ~( H1 {5 r6 O; k# K
glColor3f(0.0f, 1.0f, 0.0f);
- }6 ?* U) k. E% Y glRasterPos2f(-0.7f, -0.6f);
+ `1 a# l- A, j$ s9 J$ m drawCNString("傳統的中國漢字"); ' E4 Y# s! u$ a a- v6 L1 o
9 Q5 P6 n! Z% C L( _& N& f4 `
glutSwapBuffers(); & F ?6 A' {- R. f7 A
} 4 y& d+ u, L3 `8 k7 c( |8 Q! c N0 D
, B ]4 v; F8 N) _3 f
/ f0 ^9 L" |+ T7 x6 j' i: `1 Y
效果如图:' C: _# y# c8 s6 q
|