輪廓字體(Outline Fonts)
這節(jié)課繼續(xù)上一節(jié)課課的內(nèi)容。在第13課我們學(xué)習(xí)了如何使用位圖字體,這節(jié)課,我們將學(xué)習(xí)如何使用輪廓字體。
創(chuàng)建輪廓字體的方法類似于在第13課中我們創(chuàng)建位圖字體的方法。但是,輪廓字體看起來要酷100倍!你可以指定輪廓字體的大小。輪廓字體可以在屏幕中以3D方式運動,而且輪廓字體還可以有一定的厚度!而不是平面的2D字符。使用輪廓字體,你可以將你的計算機中的任何字體轉(zhuǎn)換為OpenGL中的3D字體,加上合適的法線,在有光照的時候,字符就會被很好的照亮了。
一個小注釋,這段代碼是專門針對Windows寫的,它使用了Windows的wgl函數(shù)來創(chuàng)建字體,顯然,Apple機系統(tǒng)有agl,X系統(tǒng)有g(shù)lx來支持做同樣事情的,不幸的是,我不能保證這些代碼也是容易使用的。如果哪位有能在屏幕上顯示文字且獨立于平臺的代碼,請告訴我,我將重寫一個有關(guān)字體的教程。
我們從第一課的典型代碼開始,添加上stdio.h頭文件以便進行標(biāo)準(zhǔn)輸入/輸出操作,另外,stdarg.h頭文件用來解析文字以及把變量轉(zhuǎn)換為文字。最后加上math.h頭文件,這樣我們就可以使用SIN和COS函數(shù)在屏幕中移動文字了。
#include <windows.h> // Header File For Windows
#include <math.h> // Header File For Windows Math Library
#include <stdio.h> // Header File For Standard Input/Output
#include <stdarg.h> // Header File For Variable Argument Routines
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <gl\glaux.h> // Header File For The Glaux Library
HDC hDC=NULL; // Private GDI Device Context
HGLRC hRC=NULL; // Permanent Rendering Context
HWND hWnd=NULL; // Holds Our Window Handle
HINSTANCE hInstance; // Holds The Instance Of The Application 另外,我們還要添加2個變量。base將保存我們創(chuàng)建的第一個顯示列表的編號。每個字符都需要有自己的顯示列表。例如,字符‘A’在顯示列表中是65,‘B’是66,‘C’是67,等等。所以,字符‘A’應(yīng)保存在顯示列表中的base + 65這個位置。
我們再添加一個叫做rot的變量。用它配合SIN和COS函數(shù)在屏幕上旋轉(zhuǎn)文字。我們同時用它來改變文字的顏色。
GLuint base; // Base Display List For The Font Set
GLfloat rot; // Used To Rotate The Text
bool keys[256]; // Array Used For The Keyboard Routine
bool active=TRUE; // Window Active Flag Set To TRUE By Default
bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default GLYPHMETRICSFLOAT gmf[256]用來保存256個輪廓字體顯示列表中對應(yīng)的每一個列表的位置和方向的信息。我們通過gmf[num]來選擇字母。num就是我們想要了解的顯示列表的編號。在稍后的代碼中,我將說明如何如何檢查每個字符的寬度,以便自動將文字定位在屏幕中心。切記,每個字符的寬度可以不相同。Glyphmetrics會大大簡化我們的工作。
GLYPHMETRICSFLOAT gmf[256];// Storage For Information About Our Outline Font Characters
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);//Declaration For WndProc下面這段用來構(gòu)建真正的字體的代碼類似于我們創(chuàng)建位圖字體的方法。和13課一樣,這段代碼是最難解釋的部分。
‘HFONT font’用來保存我們使用的Windows字體的ID。
接下來我們定義base。我們使用glGenLists(256)創(chuàng)建一組共256個顯示列表。在顯示列表創(chuàng)建好以后,變量base將保存第一個顯示列表的編號。
GLvoid BuildFont(GLvoid) // Build Our Bitmap Font
{
HFONT font; // Windows Font ID
base = glGenLists(256); // Storage For 256 Characters還有更多有趣的部分,我們將創(chuàng)建屬于自己的輪廓字體。我們從指定字體的大小開始,你會注意到它是一個負數(shù),我們通過加上一個負號來告訴Windows尋找一個基于CHARACTER高度的字體。如果我們使用一個正數(shù),就是尋找一個與基于CELL的高度相匹配的字體。
font = CreateFont( -12, // Height Of Font然后我們指定每個單元的寬度,你會注意到我把它定義為0,這樣,Windows就會使用默認值。如果你愿意的話,可以改變它的值,比如更寬一點,等等。
0, // Width Of FontAngle Of Escapement會將字體旋轉(zhuǎn)。MSDN幫助中解釋Orientation Angle用于指定每個字的底邊和顯示設(shè)備的X軸之間的角度,每個單位是十分之一個角度,不幸的是我對這個沒有概念。
0, // Angle Of Escapement
0, // Orientation Angle字體重量是一個很重要的參數(shù),你可以設(shè)置一個0-1000之間的值或使用一個已定義的值。FW_DONTCARE是0, FW_NORMAL是400, FW_BOLD是700 and FW_BLACK是900。還有許多預(yù)先定義的值,但是這四個的效果比較好。值越大,字體就越粗。
FW_BOLD, // Font WeightItalic(斜體),Underline(下劃線)和Strikeout(刪除線)可以是TRUE或FALSE。如果將Underline設(shè)置為TRUE,那么字體就會帶有下劃線,否則就沒有,非常簡單。
FALSE, // Italic
FALSE, // Underline
FALSE, // StrikeoutCharacter Set Identifier(字符集標(biāo)識符)用來描述你要使用的字符集(內(nèi)碼)類型。有太多需要說明的類型了。CHINESEBIG5_CHARSET,GREEK_CHARSET,RUSSIAN_CHARSET,DEFAULT_CHARSET ,等等。我使用的是ANSI,盡管DEFAULT也是很好用的。
如果你有興趣使用Webdings或Wingdings等字體,你必須使用SYMBOL_CHARSET而不是ANSI_CHARSET。
ANSI_CHARSET, // Character Set IdentifierOutput Precision(輸出精度)非常重要。它告訴Windows在有多種字符集的情況下使用哪類字符集。OUT_TT_PRECIS告訴Windows如果一個名字對應(yīng)多種不同的選擇字體,那么選擇字體的TRUETYPE類型。Truetype字體通常看起來要好些,尤其是你把它們放大的時候。你也可以使用OUT_TT_ONLY_PRECIS,它將會一直嘗試使用一種TRUETYPE類型的字體
OUT_TT_PRECIS, // Output Precision裁剪精度是一種當(dāng)字體落在裁剪范圍之外時使用的剪輯類型,不用多說,只要把它設(shè)置為DEFAULT就可以了。
CLIP_DEFAULT_PRECIS, // Clipping Precision輸出質(zhì)量非常重要。你可以使用PROOF,DRAFT,NONANTIALIASED,DEFAULT或ANTIALISED。
我們都知道,ANTIALIASED字體看起來很好,將一種字體Antialiasing(反鋸齒)可以實現(xiàn)在Windows下打開字體平滑時同樣的效果,它使任何東西看起來都要少些鋸齒,也就是更平滑。
ANTIALIASED_QUALITY, // Output Quality下面是Family和Pitch設(shè)置。Pitch屬性有DEFAULT_PITCH,F(xiàn)IXED_PITCH和VARIABLE_PITCH,F(xiàn)amily有FF_DECORATIVE,FF_MODERN,FF_ROMAN,FF_SCRIPT,FF_SWISS,FF_DONTCARE.嘗試一下這些值,你就會知道它們到底有什么功能。我把它們都設(shè)置為默認值。
FF_DONTCARE|DEFAULT_PITCH, // Family And Pitch最后,是我們要用的字體的確切的名字。打開Microsoft Word或其它什么文字處理軟件,點擊字體下拉菜單,找一個你喜歡的字體。將‘Comic Sans MS’替換為你想用的字體的名字,你就可以使用它了。(中文還不行,需要別的方法)
"Comic Sans MS"); // Font Name現(xiàn)在,將剛才創(chuàng)建的字體選入我們的DC。
SelectObject(hDC, font); // Selects The Font We Want
}下面是新的代碼。我們使用一個新的命令wglUseFontOutlines來創(chuàng)建輪廓字體。我們選擇我們的DC,首字符,將要創(chuàng)建的字符的個數(shù)和‘base’顯示列表值。所有一切都與我們創(chuàng)建位圖字體的方法類似。
wglUseFontOutlines( hDC, // Select The Current DC
0, // Starting Character
255, // Number Of Display Lists To Build
base, // Starting Display Lists但是,這還不是全部。接下來我們設(shè)置偏差等級。這個值越接近0.0f, 字體看起來就越平滑。設(shè)置完偏差值后,我們設(shè)置字體的厚度。它用來描述字體在Z軸上的厚度。0.0f會產(chǎn)生一個平面的2D字體,1.0f會產(chǎn)生一個有一定厚度的字體。
參數(shù)WGL_FONT_POLYGONS告訴OpenGL使用多邊形來創(chuàng)建一個實心字體。如果我們使用WGL_FONT_LINES的話,字體就變成了輪廓線(由線組成),值得注意的是,如果使用GL_FONT_LINES,就不能設(shè)置法線,光照也就不能正常工作。
最后一個參數(shù)gmf指向顯示列表數(shù)據(jù)的地址緩存。
0.0f, // Deviation From The True Outlines
0.2f, // Font Thickness In The Z Direction
WGL_FONT_POLYGONS, // Use Polygons, Not Lines
gmf); // Address Of Buffer To Recieve Data接下來的代碼很簡單。它在內(nèi)存中從base開始刪除256個顯示列表。我不知道Windows是否會做這些工作,但還是保險為好。
GLvoid KillFont(GLvoid) // Delete The Font List
{
glDeleteLists(base, 256); // Delete All 256 Characters
}
下面就是我優(yōu)異的GL文字程序了。你可以通過調(diào)用glPrint(“需要寫的文字”)來調(diào)用這段代碼。與第13課中繪制位圖字體的方法完全相同,文字被存儲在字符串 * fmt中。
GLvoid glPrint(const char *fmt, ...) // Custom GL "Print" Routine
{
下面的第一行定義了一個叫做length的變量。我們使用這個變量來查詢字符串的長度。第二行創(chuàng)建了一個大小為256個字符的字符數(shù)組,里面保存我們想要的文字串。第三行創(chuàng)建了一個指向一個變量列表的指針,我們在傳遞字符串的同時也傳遞了這個變量列表。如果我們傳遞文字時也傳遞了變量,這個指針將指向它們。
float length = 0; // Used To Find The Length Of The Text
char text[256]; // Holds Our String
va_list ap; // Pointer To List Of Arguments下面兩行代碼檢查是否有需要顯示的內(nèi)容,如果什么也沒有,fmt就等于空(NULL),屏幕上也就什么都沒有。
if (fmt == NULL) // If There's No Text
return; // Do Nothing接下來三行代碼將文字中的所有符號轉(zhuǎn)換為它們的字符編號。最后,文字和轉(zhuǎn)換的符號被存儲在一個叫做“text”的字符串中。以后我會多解釋一些有關(guān)字符的細節(jié)。
va_start(ap, fmt); // Parses The String For Variables
vsprintf(text, fmt, ap); // And Converts Symbols To Actual Numbers
va_end(ap); // Results Are Stored In Text感謝Jim Williams對下面一段代碼的建議。以前我是用手工將文字置于中心的,而他的辦法要好的多。
我們從一個循環(huán)開始,它將逐個檢查文本中的字符。我們通過strlen(text)得到文本的長度。設(shè)置好了循環(huán)以后,我們將通過加上每個字符的長度來增加length的值。當(dāng)循環(huán)結(jié)束以后,被保存在length中的值就是整個字符串的長度。所以,如果我們要寫的是“hello”,假設(shè)每個字符的長度都為10個單位,我們先給length的值加上第一個字母的長度10。然后,我們檢查第二個字母的長度,它的長度也是10,所以length就變成10 + 10(20)。當(dāng)我們檢查完所有5個字母以后,length的值就會等于50(5 *10)。
給出我們每個字符的長度的代碼是gmf[text[loop]].gmfCellIncX。記住,gmf存儲了我們每個顯示列表的信息。如果loop等于0,text[loop]就是我們的字符串中的第一個字符。如果loop等于1,text[loop]就是我們的字符串中的第二個字符。gmfCellIncX告訴我們被選擇的字符的長度。GmfCellIncX表示顯示位置從已繪制上的上一個字符向右移動的真正距離,這樣,字符之間就不會重疊在一起。同時,這個距離就是我們想得到的字符的寬度。你還可以通過gmfCelllncY命令來得到字符的高度。如果你是在垂直方向繪制文本而不是在水平方向時,這會很方便。
for (unsigned int loop=0;loop<(strlen(text));loop++)// Loop To Find Text Length
{
length+=gmf[text[loop]].gmfCellIncX;//IncreaseLength By Each Characters Width
}
最后我們?nèi)〕鲇嬎愫蟮玫降膌ength,并把它變成負數(shù)(因為我們要將文本從屏幕中心左移從而把整個文本置于屏幕中間)。然后我們把length除以2。我們并不想移動整個文本的長度,只需要一半!
glTranslatef(-length/2,0.0f,0.0f); // Center Our Text On The Screen然后我們將GL_LIST_BIT壓入屬性堆棧,它會防止glListBase影響到我們的程序中的其它顯示列表。
glListBase(base)函數(shù)用來告訴OpenGL到哪里去找每個字符對應(yīng)的顯示列表。
glPushAttrib(GL_LIST_BIT); // Pushes The Display List Bits
glListBase(base); // Sets The Base Character to 32現(xiàn)在OpenGL知道字符的存放位置了,我們就可以讓它在屏幕上顯示文字了。GlCallLists會調(diào)用多個顯示列表從而把整個文字的內(nèi)容同時顯示在屏幕上。
下面的代碼做后續(xù)工作。首先,它告訴OpenGL我們將要在屏幕上顯示出顯示列表中的內(nèi)容。Strlen(text)函數(shù)用來計算我們將要顯示在屏幕上的文字的長度。然后,OpenGL需要知道我們允許發(fā)送給它的列表的最大值。我們依然不能發(fā)送長度大于255的字符串。所以我們使用UNSIGNED_BYTE。(用0 - 255來表示我們需要的字符)。最后,我們通過傳遞字符串文字告訴OpenGL顯示什么內(nèi)容。
也許你想知道為什么字符不會彼此重疊堆積在一起。那時因為每個字符的顯示列表都知道字符的右邊緣在那里,在寫完一個字符后,OpenGL自動移動到剛寫過的字符的右邊,在寫下一個字或畫下一個物體時就會從GL移動到的最后的位置開始,也就是最后一個字符的右邊。
最后,我們將GL_LIST_BIT屬性彈出堆棧,將GL恢復(fù)到我們使用glListBase(base)設(shè)置base之前的狀態(tài)。
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);//DrawsThe Display ListText
glPopAttrib(); // Pops The Display List Bits
}重置尺寸的代碼與第一課中的代碼完全相同,所以在這里省略它。
在InitGL代碼的最后有幾行新的代碼。第13課中的BuildFont()還在,這些新的代碼用來制造快速并且不很鮮明的光線。Light0是大多數(shù)顯卡中預(yù)先定義過的,它可以很好的照亮場景而且不影響我的代碼。
我還添加了glEnable(GL_Color_Material)命令.因為字是3D對象,因此需要打開材質(zhì)色彩,否則使用glColor3f(r,g,b)改變顏色時就不能改變文字的顏色。如果你要在屏幕上繪制文本,那么再此之前先打開材質(zhì)色彩,寫完之后,在將它關(guān)閉,不然屏幕中的所有物體都會被上色。
int InitGL(GLvoid) // All Setup For OpenGL Goes Here
{
glShadeModel(GL_SMOOTH); // Enable Smooth Shading
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background
glClearDepth(1.0f); // Depth Buffer Setup
glEnable(GL_DEPTH_TEST); // Enables Depth Testing
glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);//Really Nice Perspective Calculations
glEnable(GL_LIGHT0) // Enable Default Light (Quick And Dirty)
glEnable(GL_LIGHTING); // Enable Lighting
glEnable(GL_COLOR_MATERIAL); // Enable Coloring Of Material
BuildFont(); // Build The Font
return TRUE; // Initialization Went OK
}下面就是畫圖的代碼了。我們從清除屏幕和深度緩存開始。我們調(diào)用glLoadIdentity()來重置所有東西。然后我們將坐標(biāo)系向屏幕里移動十個單位。輪廓字體在透視圖模式下表現(xiàn)非常好。你將文字移入屏幕越深,文字開起來就更小。文字離你越近,它看起來就更大。
也可以使用glScalef(x,y,z)命令來操作輪廓字體。如果你想把字體放大兩倍,可以使用glScalef(1.0f,2.0f,1.0f). 2.0f 作用在y軸, 它告訴OpenGL將顯示列表的高度繪制為原來的兩倍。如果2.0f作用在x軸,那么文本的寬度將變成原來的兩倍。
int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//Clear Screen And Depth Buffer
glLoadIdentity(); // Reset The Current Modelview Matrix
glTranslatef(0.0f,0.0f,-10.0f); // Move One Unit Into The Screen
在向屏幕里移動以后,我們希望文本能旋轉(zhuǎn)起來。下面3行代碼用來在3個軸上旋轉(zhuǎn)屏幕。我將rot乘以不同的數(shù),以便每個方向上的旋轉(zhuǎn)速度不同。
glRotatef(rot,1.0f,0.0f,0.0f); // Rotate On The X Axis
glRotatef(rot*1.5f,0.0f,1.0f,0.0f); // Rotate On The Y Axis
glRotatef(rot*1.4f,0.0f,0.0f,1.0f); // Rotate On The Z Axis
下面是令人興奮的顏色循環(huán)了。照常,我們使用唯一遞增的變量(rot)。顏色通過使用COS和SIN來循環(huán)變化。我將rot除以不同的數(shù),這樣每種顏色會以不同的速度遞增。最終的效果非常好。
// Pulsing Colors Based On The Rotation
glColor3f(1.0f*float(cos(rot/20.0f)),1.0f*float(sin(rot/25.0f)),1.0f-0.5f * float(cos(rot/17.0f)));我最喜歡的部分,將文字寫到屏幕上。我使用同將位圖字體寫到屏幕上相同的函數(shù)。將文字寫在屏幕上,所有你要做的就是glPrint(“你想寫的文字”)。很簡單。
在下面的代碼中,我們要寫的是NeHe,空格,破折號,空格,然后是rot的值除以50后的結(jié)果(為了減慢計數(shù)器)。如果這個數(shù)大于999.99,左邊第四個數(shù)將被去掉(我們要求只顯示小數(shù)點左邊3位數(shù)字)。只顯示小數(shù)點右邊的兩位數(shù)字。
glPrint("NeHe - %3.2f",rot/50); // Print GL Text To The Screen然后增大旋轉(zhuǎn)變量從而改變顏色并旋轉(zhuǎn)文字。
rot+=0.5f; // Increase The Rotation Variable
return TRUE; // Everything Went OK
}
最后,如下所示,就是在KillGLWindow()函數(shù)最后增加KillFont()函數(shù),這很重要,它在我們退出程序之前做清理工作。
if (!UnregisterClass("OpenGL",hInstance)) // Are We Able To Unregister Class
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // Set hInstance To NULL
}
KillFont(); // Destroy The Font 在這節(jié)課結(jié)束的時候,你應(yīng)該已經(jīng)學(xué)會在你的OpenGL程序中使用輪廓字體了。就像第13課,我曾在網(wǎng)上尋找一篇與這一課相似的教程,但是也沒有找到。或許我的網(wǎng)站是第一個涉及這個主題同時又把它解釋的簡單易懂的C代碼的網(wǎng)站吧。享用這篇教程,快樂編碼!