欢迎进入新的一课。这次我将会教你们使用位图字体( Bitmap Fonts),你可能会对自己说过:“把文本输出到屏幕上好难啊”。如果你尝试过,就会知道那并不是想象中那么容易。
当然,你可以运行一个图形编辑器,把文本输入并保存为图象,然后再把图象加载到OpenGL的程序中去,然后把混合打开,让文体输出到屏幕。但是这是非常耗时的,而且最后的结果可能看起来模糊或是成块,这取决于你使用哪种滤波,除非你的图象有alpha通道,一旦它被映射到屏幕上,你的文本就可以和屏幕上的物体混合起来。
如果你使用过Wordpad, Microsoft Word或是其它文字处理软件,你会注意到各种不同的类型的字体(Fonts)都可以使用。这课就将教你怎样在你自己的OpenGL程序里使用这些字体。实际上,你在计算机中安装的任何一种字体都可以在你的demo中使用。
Bitmap Font不仅可以比图形化后的纹理字体好100倍,而且你可以任意改变文本。而且不需要特地为每个词或字母产生对应的纹理。只要定位文本,使用我们新的方便的glPrint()命定就可以将文本显示在屏幕上。
我尽量让这条命令简单易懂。所以你需要做的就是键入glPrint(“Hello”)。就是那么简单。你可以用很长的介绍来说你因为这份教程而非常开心,然而写这个程序花费我大约1.5个小时。为什么这么长呢?因为并没有任何可以得到的关于使用Bitmap Font的文字信息,当然,除非你喜欢MFC代码。为了让这代码简单,我决定用简单易懂的C代码来写程序。J
小小的说明:这些代码是与windows相关的。它使用了Windows的wgl函数来产生字体。显然,Apple有agl来支持其平台,X有glx。不幸的是,我不能保证这些代码可以被移植的。如果任何人有平台无关的代码在屏幕上画字体,请将它发送给我,然后我将会写另一个教程。
我们将从Lesson 1中的典型的代码开始。我们将添加stdio.h用于输入/输出操作;stdarg.h头文件用来编译文本和将变量转化成文本,最后math.h文件让我们可以用SIN和COS来变化文本。
#include <windows.h> // Windows头文件
#include <math.h> // Windows数学库头文件(增加)
#include <stdio.h> // 标准输入/输出头文件(增加)
#include <stdarg.h> // 变量参数操作头文件(增加)
#include <gl\gl.h> // OpenGL32库头文件
#include <gl\glu.h> // GLu32库头文件
#include <gl\glaux.h> // GLaux库头文件
HDC hDC=NULL; // 私有GDI设备环境
HGLRC hRC=NULL; // 长期渲染环境
HWND hWnd=NULL; // 记录windows handle变量
HINSTANCE hInstance; // 记录instance handle变量
我们还将新增加3个变量。Base将会存储第一个显示列表的值.每个字母都需要其自己的显示列表。’A’字母的显示列表是65,’B’的是66,’C’的是67等等。所以’A’应该被存在第base+65个显示列表里。
接下来,我们增加两个counter: cnt1,cnt2。这些计数器将会以不同的比率来计数,以用来让文本通过SIN和COS在屏幕上移动。这将会产生在屏幕上产生一个伪随机运动。我们将会使用计数器来控制文体的颜色。(之后将会有更多关于这方面的内容)、
GLuint base; // 显示列表的第一个列表值
GLfloat cnt1; // 计数器1:用来移动文本和改变颜色
GLfloat cnt2; // 计数器2:用来移动文体和改变颜色
bool keys[256]; // 用于处理键盘消息的数组
bool active=TRUE; // 窗口活动标记,默认为TRUE
bool fullscreen=TRUE; // 全屏幕标记,默认为TRUE
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WndProc函数声明
接下来的代码将产生真正的字体。这是最难写代码的部分。用简写的英文’HFONT’告诉Windows我们将要控制一个Windows字体。oldfont是用来保存之前的字体以便以后恢复。
下面我们将来定义基。我们首先通过glGenLists(96)创建了96个显示列表。显示列表被创建后,base变量将会保存第一个list的值。
GLvoid BuildFont(GLvoid) // 创建Bitmap Font
{
HFONT font; // Windows字体ID
HFONT oldfont; // 用于保存旧的字体,将来恢复使用
base = glGenLists(96); // 分配96个字体的内存(新增)
现在,我们将要开始有趣的部分了。我们将要创建我们的字体了。我们首先设定字体的大小。你将会注意到那是一负数。我们通过增加一个负号告诉Windows帮我们找到一个基于CHARACTER高度的字体。如果我们使用正数,我们就要将通过CELL高度来做参照了。
font = CreateFont( -24, // Font的高度(新增)
当我们指定了单元宽度。你将会注意到我将它设置为0。这样Windows就会使用缺省值。你可以任意操纵这个值。可以让字体变宽等等。
Escapement的角度将会旋转字体。不幸的是,这不是一个很有用的功能。除非你使用了0,90,180和270度,否则这些字体将会被其不可见的四边框体裁减。从MSDN引用的Orientation Angle(定向角)以10度为单位,指定了一个角度,这个角度是字体的基线与设备的x轴的夹角。不幸的是我并不知道它的含义是什么。L
0, // Escapement角度
0, // 方向角
字体权重是一个很棒的参数。你可以从预先定义好的0-1000中选一个来使用。FW-DONTCARE是0,FW_NORMAL是400,FW_BOLD是700,FW_BLACK是900。虽然有很多预定义好的值,但是这四个值显示出了非常好的差异。值越高,字体就越粗。
倾斜,下划线和线框可以被设置为TRUE或FALSE。如果下划线被设置为TRUE,字体就会被划上下划线。如果是FALSE就将不会有下划线,非常简单。J
FALSE, // 倾斜
FALSE, // 下线线
FALSE, // 线框
字符号标志描述了你希望使用字符集的类型。有很多种字体集:CHINESEBIG5_CHARSET, GREEK_CHARSET, RUSSIAN_CHARSET, DEFAULT_CHARSET等。ANSI是我使用的,虽然DEFAULT应该也没有问题。
如果你有兴趣使用比如Webdings或是Wingdings, 你需要使用SYMBOL_CHARSE而不是ANSI_CHARSET。
输出精度非常重要。它告诉Windows如果有多于一种的字符集类型可以使用时,将要使用哪一种。OUT_TT_PRECIS告诉Windows如果同一个名字有多于一种的字体,那么就使用TRUETYPE版本的字体。Truetype字体总是看起来更好些,犹其是你将他们放大时。你也可以使用OUT_TT_ONLY_PRECIS,这将会总是尝试使用TRUETYPE字体。
裁减精度是指当字体超出裁减区域时将使用哪种裁减方式。不多说这个了,就使用默认值。
CLIP_DEFAULT_PRECIS, // 裁减精度
输出质量非常重要,你可以设置PROOF, DRAFT, NONANTIALIASED, DEFAULT或是ANTIALIASED。我们知道ANTIALISED(反走样)字体看出来更好。J 字体反走字和你打开Windows字体光滑是等效的。它将会让所有的东西看起来少一些锯齿感。
ANTIALIASED_QUALITY, // 输出质量
下一步,我们将进行Family和Pitch设置。对于Pitch,你可以DEFAULT_PITCH, FIXED_PITCH和VARIABLE_PITCH。对于Family你可以有FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE设置。你可以尝试改变他们来明白他们的作用。我只是将它们设为缺省值。
FF_DONTCARE|DEFAULT_PITCH, // Family 和 Pitch
最后,我们将设置实际字体的名字。打开Microsoft Word或是其它的文字辑编器,单击字体下拉菜单,选一个你喜欢的字体。如果要使这字体,可以用你喜欢的字体的名字来代替’Courier New’。
OldFont存放着我们先前使用过的字体。当新字体被使用时,SelectObject将会返回被设置前的字体(或是笔,或是填充器,或是任意的GDI对象)。乍看,代码似乎是选择了新的字体,并返回了一个指针并将其保存在oldFont中。
oldfont = (HFONT)SelectObject(hDC, font); // 选择我们要使用的字体
//创建从Character 32开始的 96个字符
wglUseFontBitmaps(hDC, 32, 96, base);
SelectObject(hDC, oldfont); // 选择我们要使用的字体
DeleteObject(font); // 删除字体
}
接下来的代码非常简单。他从内存中删除了96个显示列表,这些列表的第一个是存在base中的。我并不确定Windows是否会为我们做这件事,但是安全点总比后悔好。J
GLvoid KillFont(GLvoid) // 删除字体列表
{
glDeleteLists(base, 96); // 删除全部96个字符(新增)
}
现在要开始我的方便华丽的GL文本函数了。你可以通过glPrint(“message goes here”)来调用这一部分的代码。文本保存在char string *fmt中。
GLvoid glPrint(const char *fmt, ...) // 客户GL”Print”函数
{
下面的第一行创建了一个256字符串的存储空间。Text是我们最后想到打印到屏幕上的字符串。下面的第二行创建了一个指针指向我们传入参数列表的字符串头指针。如果我们通过文本传入一些参数,这个指针就将指向他们。
char text[256]; // 保存字符串
va_list ap; // 参数列表头指针
接下来的两行代码检查是否需要显示。如果是空文本,fmt等于NULL,没有文本需要在屏幕上显示。
if (fmt == NULL) // 文本是否为空
return; // 空操作
下面的三行代码将任何文本中的符号转换成实际符号的值。最终的文本和被转换过的文本被保存在”text”字符串中。我下面还会进一步解释符号。
va_start(ap, fmt); //解析变量中的字符串
vsprintf(text, fmt, ap); //将符号转换成实际的数值
va_end(ap); //结果保存在Text中
当我们设置GL_LIST_BIT时,可以避免glListBase影响其它我们在程序中可能用到的其它显示列表。
命令glListBase(base-32)较难理解一些。比如,我们要画字母’A’,它是由65来表示的。如果没有命令glListBase(base-32),OpenGL就不知道怎样找到这个字母。它将会在显示列表65中查找它,但是如果base等于1000的话,’A’实际上将会被绪存在1065列表中。因此,通过设定基点,OpenGL就会知道从哪里可以正确的得到显示列表。我们减去32的原因是我们根本没有创建前32个显示列表。我们跳过了它们。所以我们不得不让base减去32以便让OpenGL知道。我希望上面的解释是有意义的。
glPushAttrib(GL_LIST_BIT); // 增加GL_LIST_BIT属性(新增)
glListBase(base - 32); // 将基字符设置为32(新增)
现在OpenGL知道字母的位置了,我们可以调用它们在屏幕上写文本了。glCallList是一个非常有趣的命令。他可以将多于一个的显示列表一次性显示在屏幕上。
以下的代码做了如下的事性:首先,它告诉OpenGL我们将要开始在屏幕上显示文本。Strlen(text)计算出我们要将多少个字母显示在屏幕上。下一步,我们将要显示的最大的显示列表的值告诉OpenGL。我们不会传入多于255个字符的。列表的参数被处理成一个unsigned byte数组,每个元素都在0~255内。最后,我们要通过string指针的传放来告诉OpenGL我们要显示的内容。
可能我们会奇怪为什么这些字母没有一个个堆起来。每个字母的显示列表知道字母的右边在哪里。当字母被画完后。OpenGL就平移坐标系至被画的字母的右边。下一个字母或是物体将从最后的坐标系原点处开始画,也就是刚才画好字母的右边。
最后,我们弹出GL_LIST_BIT属性,让OpenGL变成我们在设置glListBase(base-32)之前的状态。
// 画显示列表 (新增)
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);
glPopAttrib(); // 弹出GL_LIST_BIT属性(新增)
}
唯一与Init代码中不同的是BuidFont()。然后就跳转到创建字体之前的代码处,然后OpenGL随后就可以使用它了。
int InitGL(GLvoid) // 所有的OpenGL配置从这里开始
{
glShadeModel(GL_SMOOTH); // Enable光滑阴影
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // 黑色背景
glClearDepth(1.0f); // 深度缓存配置
glEnable(GL_DEPTH_TEST); // Enable深度测试
glDepthFunc(GL_LEQUAL); // 将使用哪种深度测试
// Really Nice Perspective Calculations
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
BuildFont(); // 创建字体
return TRUE; // 所有的初始化成功
}
下面要开始绘制的代码了。我们首先清屏和清除深度缓存。我们调用glLoadIdentity()重置所有。然后我们向屏幕内平移1单位(即z轴负向)。如果我们不平移,文本将不会被显示出来。Bitmap Font在使用正交投影下将比透视投影下有更好的效果,但是正交投影看起来很糟糕,所以我们使用透视投影,变换。
你将会注意到,如果你平移屏幕内方向更远的位置,字体的大小并不会和你预期那样变小。当你平移更深入的时候,实际上你有了更多呆以在屏幕上放置文本的地方。如果你向内平移一个单闪,你可以把文本放置在x从-0.5到+0.5的位置内。如果你向内平移10单位,你可以把文本放置在x从-5到+5的位置内。那仅仅是让你有了更大的控制范围而不是使用更精确的坐标来准确定位文本的位置。什么都不会改变文本的大小。即使是glScalef(x,y,z)也不行。如果你想让字本变大或变小,那就创建更大或更小的字体吧!
int DrawGLScene(GLvoid) // 这是我们做所有的绘制的函数
{
// Clear The Screen And The Depth Buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity(); // 重置视角
glTranslatef(0.0f,0.0f,-1.0f); // 向内平移1单位
现在,我们要使用美妙的数字来让颜色跳变。如果你不明白我在做什么你也不用担心。我喜欢用尽量多的变量和乏味的技巧来达到最终的效果。J
这次,我使用两个计数器来让文本在屏幕内移动和在红、绿、蓝三种颜色变换。红色将使用COS和计数器1来控制,它的范围从-1.0到1.0。绿色将使用SIN和计数器2来控制,它的范围也是从-1.0到1.0。蓝色将使用COS和计数器1计数器2来控制,它的范围是从0.5到1.5。那样的话,蓝色永远不会是0,文本永远不会完全消失。并不是很有技巧,但是那样可以正常的工作。J
// 跟据文本的位置跳变颜色
glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),
1.0f-0.5f*float(cos(cnt1+cnt2)));
现在有一个新的命令,glRasterPos2f(x,y)将会把字本定位到屏幕上。屏幕的中心仍然是0,0。请注意,没有z坐标。Bitmap Font只使用x(左/右)和y(上/下)轴。因为我们向内平移了一个单位,最左边是-0.5,最右边是+0.5。你将会注意到,我沿着x轴向左平移了0.45像素。这样将文本移到了屏幕的中心。否则它将更偏右屏幕的右边一些,因为它是从中心向右开始绘制的。
让物体移动的方法和让颜色改变的方法相似。它使文本在x轴的从-0.50到-0.40范围内移动(请记住,我们减去了0.45,使其右移)。这样可以使得文本不会溢出屏幕。它通过计数器1和COS函数控制其左右摆动。它在y的-0.35到+0.35范围内移动是通过SIN和计数器2来控制的。
// 文本在屏幕上的位置
glRasterPos2f(-0.45f+0.05f*float(cos(cnt1)), 0.35f*float(sin(cnt2)));
现在是我最喜欢的部分了…将实际的文本绘到屏幕上。我让它变得超级容易而且非常用户友善。你将会发现它和一个OpenGL调用非常相似,而且结合了非常好旧式的打印描述。J所有你要做的事情就是glPrint(“{any text you want}”)。就那么简单。文本将会被确准地画在你指定的位置上。
Shanwn T寄给我修改过的代码,允许glPrint将变量打印到屏幕上。也就是说你可以增加计数器然后把结果显示在屏幕上!它是这样工作的…在我们的正文下方。有一个空格、一条横线、一个空格接着是一个“符号”(%7.2f)。现在你看到%7.2f可能会说那是什么意思。那非常简单,%就像是一个标志说明:不要将7.2f打印到屏幕上,因为它代表了一个参数。7代表在整数位最多显示7位数字。在小数点后面是2。2代表小数部分只显示2位。最后是f,f代表我们想要显示的数字是浮点类型的。我们想要将cnt1的值显示在屏幕上。举个例吧,如果cnt1等于300.12345f,那么在屏幕上显示的数字是300.12。小数点后3,4,5被截断了因为我们只想让2个小数位显示出来。
我知道如果你是一个非常有经验的C程序员,这绝对是非常基本的常识,但是有人可能从来没有用过printf。如果你对这些标志很有兴趣,可以买本书或查MSDN。
// 将GL文本打印在屏幕上
glPrint("Active OpenGL Text With NeHe - %7.2f", cnt1);
最后我们要做的事情是用不的数量让计数器增加,以便让颜色跳变和文本移动。
cnt1+=0.051f; // 增加第一个计数器
cnt2+=0.005f; // 增加第二个计数器
return TRUE; // 一切OK
}
最后要做的事情是在KillGLWindow()未尾处增加KillFont(),正如我将要展示的一样。增加这行非常重要。它在我们退出程序之前清理了内存。
if (!UnregisterClass("OpenGL",hInstance)) // 判断我们是否可以取消注册
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // 将hInstance置0
}
KillFont(); // 消毁字体
}
就是这样了…上面就是为了在自己的OpenGL项目中使用Bitmap Font字体所需要知道的全部东西。我曾经搜索网络,寻找与我们这课相似的课程,但什么都没有找到。也许我的网站是第一个以容易理解的C语言来讲这个话题的网站。不管怎样,享受这份教程和快乐的编程吧!