歡迎進(jìn)入新的一課。這次我將會(huì)教你們使用位圖字體( Bitmap Fonts),你可能會(huì)對(duì)自己說(shuō)過(guò):“把文本輸出到屏幕上好難啊”。如果你嘗試過(guò),就會(huì)知道那并不是想象中那么容易。
當(dāng)然,你可以運(yùn)行一個(gè)圖形編輯器,把文本輸入并保存為圖象,然后再把圖象加載到OpenGL的程序中去,然后把混合打開(kāi),讓文體輸出到屏幕。但是這是非常耗時(shí)的,而且最后的結(jié)果可能看起來(lái)模糊或是成塊,這取決于你使用哪種濾波,除非你的圖象有alpha通道,一旦它被映射到屏幕上,你的文本就可以和屏幕上的物體混合起來(lái)。
如果你使用過(guò)Wordpad, Microsoft Word或是其它文字處理軟件,你會(huì)注意到各種不同的類型的字體(Fonts)都可以使用。這課就將教你怎樣在你自己的OpenGL程序里使用這些字體。實(shí)際上,你在計(jì)算機(jī)中安裝的任何一種字體都可以在你的demo中使用。
Bitmap Font不僅可以比圖形化后的紋理字體好100倍,而且你可以任意改變文本。而且不需要特地為每個(gè)詞或字母產(chǎn)生對(duì)應(yīng)的紋理。只要定位文本,使用我們新的方便的glPrint()命定就可以將文本顯示在屏幕上。
我盡量讓這條命令簡(jiǎn)單易懂。所以你需要做的就是鍵入glPrint(“Hello”)。就是那么簡(jiǎn)單。你可以用很長(zhǎng)的介紹來(lái)說(shuō)你因?yàn)檫@份教程而非常開(kāi)心,然而寫(xiě)這個(gè)程序花費(fèi)我大約1.5個(gè)小時(shí)。為什么這么長(zhǎng)呢?因?yàn)椴](méi)有任何可以得到的關(guān)于使用Bitmap Font的文字信息,當(dāng)然,除非你喜歡MFC代碼。為了讓這代碼簡(jiǎn)單,我決定用簡(jiǎn)單易懂的C代碼來(lái)寫(xiě)程序。J
小小的說(shuō)明:這些代碼是與windows相關(guān)的。它使用了Windows的wgl函數(shù)來(lái)產(chǎn)生字體。顯然,Apple有agl來(lái)支持其平臺(tái),X有glx。不幸的是,我不能保證這些代碼可以被移植的。如果任何人有平臺(tái)無(wú)關(guān)的代碼在屏幕上畫(huà)字體,請(qǐng)將它發(fā)送給我,然后我將會(huì)寫(xiě)另一個(gè)教程。
我們將從Lesson 1中的典型的代碼開(kāi)始。我們將添加stdio.h用于輸入/輸出操作;stdarg.h頭文件用來(lái)編譯文本和將變量轉(zhuǎn)化成文本,最后math.h文件讓我們可以用SIN和COS來(lái)變化文本。
#include <windows.h> // Windows頭文件
#include <math.h> // Windows數(shù)學(xué)庫(kù)頭文件(增加)
#include <stdio.h> // 標(biāo)準(zhǔn)輸入/輸出頭文件(增加)
#include <stdarg.h> // 變量參數(shù)操作頭文件(增加)
#include <gl\gl.h> // OpenGL32庫(kù)頭文件
#include <gl\glu.h> // GLu32庫(kù)頭文件
#include <gl\glaux.h> // GLaux庫(kù)頭文件
HDC hDC=NULL; // 私有GDI設(shè)備環(huán)境
HGLRC hRC=NULL; // 長(zhǎng)期渲染環(huán)境
HWND hWnd=NULL; // 記錄windows handle變量
HINSTANCE hInstance; // 記錄instance handle變量
我們還將新增加3個(gè)變量。Base將會(huì)存儲(chǔ)第一個(gè)顯示列表的值.每個(gè)字母都需要其自己的顯示列表。’A’字母的顯示列表是65,’B’的是66,’C’的是67等等。所以’A’應(yīng)該被存在第base+65個(gè)顯示列表里。
接下來(lái),我們?cè)黾觾蓚€(gè)counter: cnt1,cnt2。這些計(jì)數(shù)器將會(huì)以不同的比率來(lái)計(jì)數(shù),以用來(lái)讓文本通過(guò)SIN和COS在屏幕上移動(dòng)。這將會(huì)產(chǎn)生在屏幕上產(chǎn)生一個(gè)偽隨機(jī)運(yùn)動(dòng)。我們將會(huì)使用計(jì)數(shù)器來(lái)控制文體的顏色。(之后將會(huì)有更多關(guān)于這方面的內(nèi)容)、
GLuint base; // 顯示列表的第一個(gè)列表值
GLfloat cnt1; // 計(jì)數(shù)器1:用來(lái)移動(dòng)文本和改變顏色
GLfloat cnt2; // 計(jì)數(shù)器2:用來(lái)移動(dòng)文體和改變顏色
bool keys[256]; // 用于處理鍵盤(pán)消息的數(shù)組
bool active=TRUE; // 窗口活動(dòng)標(biāo)記,默認(rèn)為TRUE
bool fullscreen=TRUE; // 全屏幕標(biāo)記,默認(rèn)為TRUE
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WndProc函數(shù)聲明
接下來(lái)的代碼將產(chǎn)生真正的字體。這是最難寫(xiě)代碼的部分。用簡(jiǎn)寫(xiě)的英文’HFONT’告訴Windows我們將要控制一個(gè)Windows字體。oldfont是用來(lái)保存之前的字體以便以后恢復(fù)。
下面我們將來(lái)定義基。我們首先通過(guò)glGenLists(96)創(chuàng)建了96個(gè)顯示列表。顯示列表被創(chuàng)建后,base變量將會(huì)保存第一個(gè)list的值。
GLvoid BuildFont(GLvoid) // 創(chuàng)建Bitmap Font
{
HFONT font; // Windows字體ID
HFONT oldfont; // 用于保存舊的字體,將來(lái)恢復(fù)使用
base = glGenLists(96); // 分配96個(gè)字體的內(nèi)存(新增)
現(xiàn)在,我們將要開(kāi)始有趣的部分了。我們將要?jiǎng)?chuàng)建我們的字體了。我們首先設(shè)定字體的大小。你將會(huì)注意到那是一負(fù)數(shù)。我們通過(guò)增加一個(gè)負(fù)號(hào)告訴Windows幫我們找到一個(gè)基于CHARACTER高度的字體。如果我們使用正數(shù),我們就要將通過(guò)CELL高度來(lái)做參照了。
font = CreateFont( -24, // Font的高度(新增)
當(dāng)我們指定了單元寬度。你將會(huì)注意到我將它設(shè)置為0。這樣Windows就會(huì)使用缺省值。你可以任意操縱這個(gè)值。可以讓字體變寬等等。
Escapement的角度將會(huì)旋轉(zhuǎn)字體。不幸的是,這不是一個(gè)很有用的功能。除非你使用了0,90,180和270度,否則這些字體將會(huì)被其不可見(jiàn)的四邊框體裁減。從MSDN引用的Orientation Angle(定向角)以10度為單位,指定了一個(gè)角度,這個(gè)角度是字體的基線與設(shè)備的x軸的夾角。不幸的是我并不知道它的含義是什么。L
0, // Escapement角度
0, // 方向角
字體權(quán)重是一個(gè)很棒的參數(shù)。你可以從預(yù)先定義好的0-1000中選一個(gè)來(lái)使用。FW-DONTCARE是0,FW_NORMAL是400,FW_BOLD是700,FW_BLACK是900。雖然有很多預(yù)定義好的值,但是這四個(gè)值顯示出了非常好的差異。值越高,字體就越粗。
傾斜,下劃線和線框可以被設(shè)置為TRUE或FALSE。如果下劃線被設(shè)置為TRUE,字體就會(huì)被劃上下劃線。如果是FALSE就將不會(huì)有下劃線,非常簡(jiǎn)單。J
FALSE, // 傾斜
FALSE, // 下線線
FALSE, // 線框
字符號(hào)標(biāo)志描述了你希望使用字符集的類型。有很多種字體集:CHINESEBIG5_CHARSET, GREEK_CHARSET, RUSSIAN_CHARSET, DEFAULT_CHARSET等。ANSI是我使用的,雖然DEFAULT應(yīng)該也沒(méi)有問(wèn)題。
如果你有興趣使用比如Webdings或是Wingdings, 你需要使用SYMBOL_CHARSE而不是ANSI_CHARSET。
ANSI_CHARSET, // 字符集標(biāo)志
輸出精度非常重要。它告訴Windows如果有多于一種的字符集類型可以使用時(shí),將要使用哪一種。OUT_TT_PRECIS告訴Windows如果同一個(gè)名字有多于一種的字體,那么就使用TRUETYPE版本的字體。Truetype字體總是看起來(lái)更好些,猶其是你將他們放大時(shí)。你也可以使用OUT_TT_ONLY_PRECIS,這將會(huì)總是嘗試使用TRUETYPE字體。
裁減精度是指當(dāng)字體超出裁減區(qū)域時(shí)將使用哪種裁減方式。不多說(shuō)這個(gè)了,就使用默認(rèn)值。
CLIP_DEFAULT_PRECIS, // 裁減精度
輸出質(zhì)量非常重要,你可以設(shè)置PROOF, DRAFT, NONANTIALIASED, DEFAULT或是ANTIALIASED。我們知道ANTIALISED(反走樣)字體看出來(lái)更好。J 字體反走字和你打開(kāi)Windows字體光滑是等效的。它將會(huì)讓所有的東西看起來(lái)少一些鋸齒感。
ANTIALIASED_QUALITY, // 輸出質(zhì)量
下一步,我們將進(jìn)行Family和Pitch設(shè)置。對(duì)于Pitch,你可以DEFAULT_PITCH, FIXED_PITCH和VARIABLE_PITCH。對(duì)于Family你可以有FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE設(shè)置。你可以嘗試改變他們來(lái)明白他們的作用。我只是將它們?cè)O(shè)為缺省值。
FF_DONTCARE|DEFAULT_PITCH, // Family 和 Pitch
最后,我們將設(shè)置實(shí)際字體的名字。打開(kāi)Microsoft Word或是其它的文字輯編器,單擊字體下拉菜單,選一個(gè)你喜歡的字體。如果要使這字體,可以用你喜歡的字體的名字來(lái)代替’Courier New’。
OldFont存放著我們先前使用過(guò)的字體。當(dāng)新字體被使用時(shí),SelectObject將會(huì)返回被設(shè)置前的字體(或是筆,或是填充器,或是任意的GDI對(duì)象)。乍看,代碼似乎是選擇了新的字體,并返回了一個(gè)指針并將其保存在oldFont中。
oldfont = (HFONT)SelectObject(hDC, font); // 選擇我們要使用的字體
//創(chuàng)建從Character 32開(kāi)始的 96個(gè)字符
wglUseFontBitmaps(hDC, 32, 96, base);
SelectObject(hDC, oldfont); // 選擇我們要使用的字體
DeleteObject(font); // 刪除字體
}
接下來(lái)的代碼非常簡(jiǎn)單。他從內(nèi)存中刪除了96個(gè)顯示列表,這些列表的第一個(gè)是存在base中的。我并不確定Windows是否會(huì)為我們做這件事,但是安全點(diǎn)總比后悔好。J
GLvoid KillFont(GLvoid) // 刪除字體列表
{
glDeleteLists(base, 96); // 刪除全部96個(gè)字符(新增)
}
現(xiàn)在要開(kāi)始我的方便華麗的GL文本函數(shù)了。你可以通過(guò)glPrint(“message goes here”)來(lái)調(diào)用這一部分的代碼。文本保存在char string *fmt中。
GLvoid glPrint(const char *fmt, ...) // 客戶GL”Print”函數(shù)
{
下面的第一行創(chuàng)建了一個(gè)256字符串的存儲(chǔ)空間。Text是我們最后想到打印到屏幕上的字符串。下面的第二行創(chuàng)建了一個(gè)指針指向我們傳入?yún)?shù)列表的字符串頭指針。如果我們通過(guò)文本傳入一些參數(shù),這個(gè)指針就將指向他們。
char text[256]; // 保存字符串
va_list ap; // 參數(shù)列表頭指針
接下來(lái)的兩行代碼檢查是否需要顯示。如果是空文本,fmt等于NULL,沒(méi)有文本需要在屏幕上顯示。
if (fmt == NULL) // 文本是否為空
return; // 空操作
下面的三行代碼將任何文本中的符號(hào)轉(zhuǎn)換成實(shí)際符號(hào)的值。最終的文本和被轉(zhuǎn)換過(guò)的文本被保存在”text”字符串中。我下面還會(huì)進(jìn)一步解釋符號(hào)。
va_start(ap, fmt); //解析變量中的字符串
vsprintf(text, fmt, ap); //將符號(hào)轉(zhuǎn)換成實(shí)際的數(shù)值
va_end(ap); //結(jié)果保存在Text中
當(dāng)我們?cè)O(shè)置GL_LIST_BIT時(shí),可以避免glListBase影響其它我們?cè)诔绦蛑锌赡苡玫降钠渌@示列表。
命令glListBase(base-32)較難理解一些。比如,我們要畫(huà)字母’A’,它是由65來(lái)表示的。如果沒(méi)有命令glListBase(base-32),OpenGL就不知道怎樣找到這個(gè)字母。它將會(huì)在顯示列表65中查找它,但是如果base等于1000的話,’A’實(shí)際上將會(huì)被緒存在1065列表中。因此,通過(guò)設(shè)定基點(diǎn),OpenGL就會(huì)知道從哪里可以正確的得到顯示列表。我們減去32的原因是我們根本沒(méi)有創(chuàng)建前32個(gè)顯示列表。我們跳過(guò)了它們。所以我們不得不讓base減去32以便讓OpenGL知道。我希望上面的解釋是有意義的。
glPushAttrib(GL_LIST_BIT); // 增加GL_LIST_BIT屬性(新增)
glListBase(base - 32); // 將基字符設(shè)置為32(新增)
現(xiàn)在OpenGL知道字母的位置了,我們可以調(diào)用它們?cè)谄聊簧蠈?xiě)文本了。glCallList是一個(gè)非常有趣的命令。他可以將多于一個(gè)的顯示列表一次性顯示在屏幕上。
以下的代碼做了如下的事性:首先,它告訴OpenGL我們將要開(kāi)始在屏幕上顯示文本。Strlen(text)計(jì)算出我們要將多少個(gè)字母顯示在屏幕上。下一步,我們將要顯示的最大的顯示列表的值告訴OpenGL。我們不會(huì)傳入多于255個(gè)字符的。列表的參數(shù)被處理成一個(gè)unsigned byte數(shù)組,每個(gè)元素都在0~255內(nèi)。最后,我們要通過(guò)string指針的傳放來(lái)告訴OpenGL我們要顯示的內(nèi)容。
可能我們會(huì)奇怪為什么這些字母沒(méi)有一個(gè)個(gè)堆起來(lái)。每個(gè)字母的顯示列表知道字母的右邊在哪里。當(dāng)字母被畫(huà)完后。OpenGL就平移坐標(biāo)系至被畫(huà)的字母的右邊。下一個(gè)字母或是物體將從最后的坐標(biāo)系原點(diǎn)處開(kāi)始畫(huà),也就是剛才畫(huà)好字母的右邊。
最后,我們彈出GL_LIST_BIT屬性,讓OpenGL變成我們?cè)谠O(shè)置glListBase(base-32)之前的狀態(tài)。
// 畫(huà)顯示列表 (新增)
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);
glPopAttrib(); // 彈出GL_LIST_BIT屬性(新增)
}
唯一與Init代碼中不同的是BuidFont()。然后就跳轉(zhuǎn)到創(chuàng)建字體之前的代碼處,然后OpenGL隨后就可以使用它了。
int InitGL(GLvoid) // 所有的OpenGL配置從這里開(kāi)始
{
glShadeModel(GL_SMOOTH); // Enable光滑陰影
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // 黑色背景
glClearDepth(1.0f); // 深度緩存配置
glEnable(GL_DEPTH_TEST); // Enable深度測(cè)試
glDepthFunc(GL_LEQUAL); // 將使用哪種深度測(cè)試
// Really Nice Perspective Calculations
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
BuildFont(); // 創(chuàng)建字體
return TRUE; // 所有的初始化成功
}
下面要開(kāi)始繪制的代碼了。我們首先清屏和清除深度緩存。我們調(diào)用glLoadIdentity()重置所有。然后我們向屏幕內(nèi)平移1單位(即z軸負(fù)向)。如果我們不平移,文本將不會(huì)被顯示出來(lái)。Bitmap Font在使用正交投影下將比透視投影下有更好的效果,但是正交投影看起來(lái)很糟糕,所以我們使用透視投影,變換。
你將會(huì)注意到,如果你平移屏幕內(nèi)方向更遠(yuǎn)的位置,字體的大小并不會(huì)和你預(yù)期那樣變小。當(dāng)你平移更深入的時(shí)候,實(shí)際上你有了更多呆以在屏幕上放置文本的地方。如果你向內(nèi)平移一個(gè)單閃,你可以把文本放置在x從-0.5到+0.5的位置內(nèi)。如果你向內(nèi)平移10單位,你可以把文本放置在x從-5到+5的位置內(nèi)。那僅僅是讓你有了更大的控制范圍而不是使用更精確的坐標(biāo)來(lái)準(zhǔn)確定位文本的位置。什么都不會(huì)改變文本的大小。即使是glScalef(x,y,z)也不行。如果你想讓字本變大或變小,那就創(chuàng)建更大或更小的字體吧!
int DrawGLScene(GLvoid) // 這是我們做所有的繪制的函數(shù)
{
// Clear The Screen And The Depth Buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity(); // 重置視角
glTranslatef(0.0f,0.0f,-1.0f); // 向內(nèi)平移1單位
現(xiàn)在,我們要使用美妙的數(shù)字來(lái)讓顏色跳變。如果你不明白我在做什么你也不用擔(dān)心。我喜歡用盡量多的變量和乏味的技巧來(lái)達(dá)到最終的效果。J
這次,我使用兩個(gè)計(jì)數(shù)器來(lái)讓文本在屏幕內(nèi)移動(dòng)和在紅、綠、藍(lán)三種顏色變換。紅色將使用COS和計(jì)數(shù)器1來(lái)控制,它的范圍從-1.0到1.0。綠色將使用SIN和計(jì)數(shù)器2來(lái)控制,它的范圍也是從-1.0到1.0。藍(lán)色將使用COS和計(jì)數(shù)器1計(jì)數(shù)器2來(lái)控制,它的范圍是從0.5到1.5。那樣的話,藍(lán)色永遠(yuǎn)不會(huì)是0,文本永遠(yuǎn)不會(huì)完全消失。并不是很有技巧,但是那樣可以正常的工作。J
// 跟據(jù)文本的位置跳變顏色
glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),
1.0f-0.5f*float(cos(cnt1+cnt2)));
現(xiàn)在有一個(gè)新的命令,glRasterPos2f(x,y)將會(huì)把字本定位到屏幕上。屏幕的中心仍然是0,0。請(qǐng)注意,沒(méi)有z坐標(biāo)。Bitmap Font只使用x(左/右)和y(上/下)軸。因?yàn)槲覀兿騼?nèi)平移了一個(gè)單位,最左邊是-0.5,最右邊是+0.5。你將會(huì)注意到,我沿著x軸向左平移了0.45像素。這樣將文本移到了屏幕的中心。否則它將更偏右屏幕的右邊一些,因?yàn)樗菑闹行南蛴议_(kāi)始繪制的。
讓物體移動(dòng)的方法和讓顏色改變的方法相似。它使文本在x軸的從-0.50到-0.40范圍內(nèi)移動(dòng)(請(qǐng)記住,我們減去了0.45,使其右移)。這樣可以使得文本不會(huì)溢出屏幕。它通過(guò)計(jì)數(shù)器1和COS函數(shù)控制其左右擺動(dòng)。它在y的-0.35到+0.35范圍內(nèi)移動(dòng)是通過(guò)SIN和計(jì)數(shù)器2來(lái)控制的。
// 文本在屏幕上的位置
glRasterPos2f(-0.45f+0.05f*float(cos(cnt1)), 0.35f*float(sin(cnt2)));
現(xiàn)在是我最喜歡的部分了…將實(shí)際的文本繪到屏幕上。我讓它變得超級(jí)容易而且非常用戶友善。你將會(huì)發(fā)現(xiàn)它和一個(gè)OpenGL調(diào)用非常相似,而且結(jié)合了非常好舊式的打印描述。J所有你要做的事情就是glPrint(“{any text you want}”)。就那么簡(jiǎn)單。文本將會(huì)被確準(zhǔn)地畫(huà)在你指定的位置上。
Shanwn T寄給我修改過(guò)的代碼,允許glPrint將變量打印到屏幕上。也就是說(shuō)你可以增加計(jì)數(shù)器然后把結(jié)果顯示在屏幕上!它是這樣工作的…在我們的正文下方。有一個(gè)空格、一條橫線、一個(gè)空格接著是一個(gè)“符號(hào)”(%7.2f)。現(xiàn)在你看到%7.2f可能會(huì)說(shuō)那是什么意思。那非常簡(jiǎn)單,%就像是一個(gè)標(biāo)志說(shuō)明:不要將7.2f打印到屏幕上,因?yàn)樗砹艘粋€(gè)參數(shù)。7代表在整數(shù)位最多顯示7位數(shù)字。在小數(shù)點(diǎn)后面是2。2代表小數(shù)部分只顯示2位。最后是f,f代表我們想要顯示的數(shù)字是浮點(diǎn)類型的。我們想要將cnt1的值顯示在屏幕上。舉個(gè)例吧,如果cnt1等于300.12345f,那么在屏幕上顯示的數(shù)字是300.12。小數(shù)點(diǎn)后3,4,5被截?cái)嗔艘驗(yàn)槲覀冎幌胱?/span>2個(gè)小數(shù)位顯示出來(lái)。
我知道如果你是一個(gè)非常有經(jīng)驗(yàn)的C程序員,這絕對(duì)是非常基本的常識(shí),但是有人可能從來(lái)沒(méi)有用過(guò)printf。如果你對(duì)這些標(biāo)志很有興趣,可以買(mǎi)本書(shū)或查MSDN。
// 將GL文本打印在屏幕上
glPrint("Active OpenGL Text With NeHe - %7.2f", cnt1);
最后我們要做的事情是用不的數(shù)量讓計(jì)數(shù)器增加,以便讓顏色跳變和文本移動(dòng)。
cnt1+=0.051f; // 增加第一個(gè)計(jì)數(shù)器
cnt2+=0.005f; // 增加第二個(gè)計(jì)數(shù)器
return TRUE; // 一切OK
}
最后要做的事情是在KillGLWindow()未尾處增加KillFont(),正如我將要展示的一樣。增加這行非常重要。它在我們退出程序之前清理了內(nèi)存。
if (!UnregisterClass("OpenGL",hInstance)) // 判斷我們是否可以取消注冊(cè)
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // 將hInstance置0
}
KillFont(); // 消毀字體
}
就是這樣了…上面就是為了在自己的OpenGL項(xiàng)目中使用Bitmap Font字體所需要知道的全部東西。我曾經(jīng)搜索網(wǎng)絡(luò),尋找與我們這課相似的課程,但什么都沒(méi)有找到。也許我的網(wǎng)站是第一個(gè)以容易理解的C語(yǔ)言來(lái)講這個(gè)話題的網(wǎng)站。不管怎樣,享受這份教程和快樂(lè)的編程吧!