前言
声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改。对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢。
NeHe OpenGL第十三课:图像字体
图像字体:
这一课我们将创建一些基于2D图像的字体,它们可以缩放,但不能旋转,并且总是面向前方,但作为基本的显示来说,我想已经够了。
欢迎来到另一课教程,这次我将教你如何使用位图字体,也许你会对自己说:“在屏幕上显示文字有什么难的?”。但是你真正尝试过就会知道,它确实没那么容易。
当然,你可以载入一段美术程序,把文字写在一个图片上,再把这幅图片载入你的OpenGL程序中,打开混合选项,从而在屏幕上显示出文字。但是这种做法非常耗时。而且根据你选择的滤波类型,最终结果常常会显得
很模糊,或者有很多马赛克。另外,除非你的图像包含一个Alpha通道,否则一旦绘制在屏幕上,那些文字就会不透明(与屏幕中的其它物体混合)。
如果你使用过记事本、微软的Word或者其它文字处理软件,你会注意到所有不同的字体都是可用的。这课就会教你如何在自己的OpenGL程序中使用和原样相同的字体。事实上,任何安装在你的计算机中的字体都可以使
用在演示中(中文不行)。
使用位图字体比起使用图形字体(贴图)看起来不止强100倍。你可以随时改变显示在屏幕上的文字,而且用不着为它们逐个制作贴图。只需要将文字定位,再使用我最新的gl命令就可以在屏幕上显示文字了。
我尽可能试着将命令做的简单。你只需要敲入glPrint("Hello") 。它是那么简单。不管怎样,从这段长长的介绍就可以看出,我对这课教程是多么的满意。写这段代码大概花了我一个半小时,为什么这么长的时间呢
?那是因为在使用位图字体方面完全没有可用的资料,除非你愿意使用MFC中的代码。为了使代码简单,我想,如果我把它全部重写为容易理解的C语言代码,那一定会好些 :)
一个小注释,这段代码是专门针对Windows写的,它使用了Windows的wgl函数来创建字体,显然,Apple机系统有agl,X系统有glx来支持做同样事情的,不幸的是,我不能保证这些代码也是容易使用的。如果那位
有能在屏幕上显示文字且独立于平台的代码,请告诉我,我将重写一个有关字体的教程。
我们从第一课的典型代码开始,添加上stdio.h头文件以便进行标准输入/输出操作,另外,stdarg.h头文件用来解析文字以及把变量转换为文字。最后加上math.h头文件,这样我们就可以使用SIN和COS函数在屏幕
中移动文字了。
#include <stdarg.h> // 用来定义可变参数的头文件
另外,我们还要添加3个变量。base将保存我们创建的第一个显示列表的编号。每个字符都需要有自己的显示列表。例如,字符‘A’在显示列表中是65,‘B’是66,‘C’是67,等等。所以,字符‘A’应保存在显示列表中
的base + 65这个位置。
然后添加两个计数器(cnt1 和 cnt2),它们采用不用的累加速度,通过SIN和COS函数来改变文字在屏幕上的位置。在屏幕上创造出一种看起来像是半随机的移动方式。同时,我们用这两个计数器来改变文字的颜色
(后面会进一步解释)。
GLuint base; // 绘制字体的显示列表的开始位置
GLfloat cnt1; // 字体移动计数器1
GLfloat cnt2; // 字体移动计数器2
下面这段代码用来构建真实的字体,这也是最难写的一部分代码。‘HFONT font’告诉Windows我们将要使用一个Windows字体。Oldfont用来存放字体。
接下来我们在定义base的同时使用glGenLists(96)创建了一组共96个显示列表。
GLvoid BuildFont(GLvoid) // 创建位图字体
{
HFONT font; // 字体句柄
HFONT oldfont; // 旧的字体句柄
base = glGenLists(96); // 创建96个显示列表
下面该有趣的部分了,我们将创建属于自己的字体。我们从指定字体的大小开始,你会注意到它是一个负数,我们通过加上一个负号来告诉Windows寻找一个基于CHARACTER高度的字体。如果我们使用一个正数,就是
寻找一个与基于CELL的高度相匹配的字体。
font = CreateFont( -24, // 字体高度
然后我们指定每个单元的宽度,你会注意到我把它定义为0,这样,Windows就会使用默认值。如果你愿意的话,可以改变它的值,比如更宽一点,等等。
0, // 字体宽度
Angle Of Escapement会将字体旋转,它不是一个常用的属性,除了0,90,180,270四个角度以外,由于字体本身要适应其看不见的方形边框,常常会显的裁切不正。MSDN帮助中解释Orientation Angle用于
指定每个字的底边和显示设备的X轴之间的角度,每个单位是十分之一个角度,不幸的是我对这个没有概念。
0, // 字体的旋转角度 Angle Of Escapement
0, // 字体底线的旋转角度Orientation Angle
字体重量是一个很重要的参数,你可以设置一个0–1000之间的值或使用一个已定义的值。FW_DONTCARE是0, FW_NORMAL是400, FW_BOLD是700 and FW_BLACK是900。还有许多预先定义的值,但是这四个的效
果比较好。值越大,字体就越粗。
FW_BOLD, // 字体的重量
Italic(斜体),Underline(下划线)和Strikeout(删除线)可以是TRUE或FALSE。如果将Underline设置为TRUE,那么字体就会带有下划线,否则就没有,非常简单。
FALSE, // 是否使用斜体
FALSE, // 是否使用下划线
FALSE, // 是否使用删除线
Character Set Identifier(字符集标识符)用来描述你要使用的字符集(内码)类型。有太多需要说明的类型了。CHINESEBIG5_CHARSET,GREEK_CHARSET,RUSSIAN_CHARSET,DEFAULT_CHARSET ,
等等。我使用的是ANSI,尽管DEFAULT也是很好用的。
如果你有兴趣使用Webdings或Wingdings等字体,你必须使用SYMBOL_CHARSET而不是ANSI_CHARSET。
ANSI_CHARSET, // 设置字符集
Output Precision(输出精度)非常重要。它告诉Windows在有多种字符集的情况下使用哪类字符集。OUT_TT_PRECIS告诉Windows如果一个名字对应多种不同的选择字体,那么选择字体的TRUETYPE类型。
Truetype字体通常看起来要好些,尤其是你把它们放大的时候。你也可以使用OUT_TT_ONLY_PRECIS,它将会一直尝试使用一种TRUETYPE类型的字体
OUT_TT_PRECIS, // 输出精度
裁剪精度是一种当字体落在裁剪范围之外时使用的剪辑类型,不用多说,只要把它设置为DEFAULT就可以了。
CLIP_DEFAULT_PRECIS, // 裁剪精度
输出质量非常重要。你可以使用PROOF,DRAFT,NONANTIALIASED,DEFAULT或ANTIALISED。
我们都知道,ANTIALIASED字体看起来很好,将一种字体Antialiasing(反锯齿)可以实现在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 And Pitch
最后,是我们需要的字体的确切的名字。打开Microsoft Word或其它什么文字处理软件,点击字体下拉菜单,找一个你喜欢的字体。将‘Courier New’替换为你想用的字体的名字,你就可以使用它了。(中文还不
行,需要别的方法)
"Courier New"); // 字体名称
现在,选择我们刚才创建的字体。Oldfont将指向被选择的对象。然后我们从第32个字符(空格)开始建立96个显示列表。如果你愿意,也可以建立所有256个字符,只要确保使用glGenLists建立256个显示列表就
可以了。然后我们将oldfont对象指针选入hDC并且删除font对象。
oldfont = (HFONT)SelectObject(hDC, font); // 选择我们需要的字体
wglUseFontBitmaps(hDC, 32, 96, base); // 创建96个显示列表,绘制从ASCII码为32-128的字符
SelectObject(hDC, oldfont); // 选择原来的字体
DeleteObject(font); // 删除字体
}
接下来的代码很简单。它在内存中从base开始删除96个显示列表。我不知道Windows是否会做这些工作,但还是保险为好。
GLvoid KillFont(GLvoid) // 删除显示列表
{
glDeleteLists(base, 96); //删除96个显示列表
}
下面就是我优异的GL文字程序了。你可以通过调用glPrint(“需要写的文字”)来调用这段代码。文字被存储在字符串 * fmt中。
GLvoid glPrint(const char *fmt, ...) // 自定义GL输出字体函数
{
下面的第一行创建了一个大小为256个字符的字符数组,里面保存我们想要的文字串。第二行创建了一个指向一个变量列表的指针。我们在传递字符串的同时也传递了这个变量列表。如果我们传递文本时也传递了变量,
这个指针将指向它们。
char text[256]; // 保存文字串
va_list ap; // 指向一个变量列表的指针
下面两行代码检查是否有需要显示的内容,如果什么也没有,fmt就等于空(NULL),屏幕上也就什么都没有。
if (fmt == NULL) // 如果无输入则返回
return;
接下来三行代码将文字中的所有符号转换为它们的字符编号。最后,文字和转换的符号被存储在一个叫做text的字符串中。以后我会多解释一些有关字符的细节。
va_start(ap, fmt); // 分析可变参数
vsprintf(text, fmt, ap); // 把参数值写入字符串
va_end(ap); // 结束分析
然后我们将GL_LIST_BIT压入属性堆栈,它会防止glListBase影响到我们的程序中的其它显示列表。
GlListBase(base-32)是一条有些难解释的命令。比如说要写字母‘A’,它的相应编号为65。如果没有glListBase(base-32)命令,OpenGL就不知道到哪去找这个字母。它会在显示列表中的第65个位置找它,但
是,假如base的值等于1000,那么‘A’的实际存放位置就是1065了。所以通过base设置一个起点,OpenGL就知道到哪去找到正确的显示列表了。减去32是因为我们没有构造过前32个显示列表,那么就跳过它们好了
。于是,我们不得不通过从base的值减去32来让OpenGL知道这一点。我希望这些有意义。
glPushAttrib(GL_LIST_BIT); // 把显示列表属性压入属性堆栈
glListBase(base - 32); // 设置显示列表的基础值
现在OpenGL知道字母的存放位置了,我们就可以让它在屏幕上显示文字了。GlCallLists是一个很有趣的命令。它可以同时将多个显示列表的内容显示在屏幕上。
下面的代码做后续工作。首先,它告诉OpenGL我们将要在屏幕上显示出显示列表中的内容。Strlen(text)函数用来计算我们将要显示在屏幕上的文字的长度。然后,OpenGL需要知道我们允许发送给它的列表的最大
值。我们不能发送长度大于255的字符串。这个字符列表的参数被当作一个无符号字符数组处理,它们的值都介于0到255之间。最后,我们通过传递text(它指向我们的字符串)来告诉OpenGL显示的内容。
也许你想知道为什么字符不会彼此重叠堆积在一起。那时因为每个字符的显示列表都知道字符的右边缘在那里,在写完一个字符后,OpenGL自动移动到刚写过的字符的右边,在写下一个字或画下一个物体时就会从GL移
动到的最后的位置开始,也就是最后一个字符的右边。
最后,我们将GL_LIST_BIT属性弹出堆栈,将GL恢复到我们使用glListBase(base-32)设置base那时的状态。
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); // 调用显示列表绘制字符串
glPopAttrib(); // 弹出属性堆栈
}
在初始化代码中唯一的变化就是BuildFont()。它调用前面的代码来创建字体,然后OpenGL就可以使用这个字体了。
BuildFont(); // 创建字体
下面就是画图的代码了。我们从清除屏幕和深度缓存开始。我们调用glLoadIdentity()来重置所有东西。然后我们将坐标系向屏幕里移动一个单位。如果不移动的话无法显示出文字。当你使用透视投影而不是ortho
投影的时候位图字体表现的更好。由于ortho看起来不好,所以我用透视投影,并移动坐标系。。
你会注意到如果把坐标系在屏幕里放的更深远,字体并不会想你想象的那样缩小,只是你可以在控制文字位置时有更多的选择。如果你将坐标系移入屏幕一个单位,你就可以字X轴上-0.5到+0.5的范围内调整文字的位
置。如果深入10个单位的话,移动范围就从-5到+5。它给了你更多的选择来替代使用小数指定文字的精确位置。什么都不能改变文字的大小,即使是调用glScale(x,y,z)函数.如果你想改变字体的大小,只能在创建
它的时候改变它。
int DrawGLScene(GLvoid) // 此过程中包括所有的绘制代码
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕及深度缓存
glLoadIdentity(); // 重置当前的模型观察矩阵
glTranslatef(0.0f,0.0f,-1.0f); // 移入屏幕一个单位
下面我们使用一些奇妙的数学方法来产生颜色变化脉冲。如果你不懂我在做什么你也不必担心。我喜欢利用教多的变量和教简单的方法来达到我的目的。
这样,我使用那两个用来改变文字在屏幕上位置的计数器来改变红、绿、蓝这些颜色。红色值使用COS和计数器1在-1.0到1.0之间变化。绿色值使用SIN和计数器2也在-1.0到1.0之间变化。蓝色值使用COS和计数器1
和2在0.5到1.5之间变化。于是,蓝色值就永远不会等于0,文字的颜色也永远不会消失。笨办法,但很管用。
// 根据字体位置设置颜色
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轴位置。位图字体只使用X轴(左/右)和Y轴(上/下)。因为我们将坐标系移入屏幕一个单位,
往左最大值为-0.5,往右最大值为+0.5。你会注意到我在X轴上向左移动了0.45个像素。它将文字移到屏幕的中心位置。否则,因为文字的起点就是屏幕的中心,会造成文字整体偏右。
计算文字位置的算法与设置文字颜色的算法差不多。它将文字在X轴的-0.50到-0.40的范围内移动(记住,我们从起点就减了0.45),这就保证文字始终能显示在屏幕内。由于使用COS和计数器1,所以文字左右摆动
,使用SIN和计数器2在Y轴的-0.35到0.35范围内移动。
// 设置光栅化位置,即字体的位置
glRasterPos2f(-0.45f+0.05f*float(cos(cnt1)), 0.35f*float(sin(cnt2)));
现在轮到我最满意的部分了。将真正的文字写到屏幕上。我试着把它做的非常简单,而且非常友好,便于使用。你会注意到它看起来像调用一个OpenGL的函数,有点类似C语言中的输出语句的风格。在屏幕上输出文字只
需要调用glPrint(“你想写的文字”).它很容易。文字将精确的显示在屏幕上你指定的位置。
Shawn T.发给我修改过的代码允许glPrint传递变量到屏幕。这意味着你可以增加一个计数器,并且在屏幕上显示出这个计数器的值,它是这样工作的。。。在下一行你看到:要显示的普通文字,然后有一个空格,一
个破折号,一个空格,然后是一个“符号”(%7.2f)(C语言中的输出格式控制字).现在你会看着%7.2说这是什么意思。它其实很简单,%是一个记号,表示不要把7.2f本身显示在屏幕上,因为它代表一个变量。7表示
小数点左边最多有7位数字。然后是小数部分,小数点右边的2表示小数点右边最多保留两位小数。最后,f表示我们想要显示的数字类型为浮点型。我们想在屏幕上显示计数器1的值。比如,计数器1的值为300.12345f
,那么在屏幕上显示的数字就是300.12,小数部分的3,4,5会舍去。因为我们只需要显示小数点后面两位数字。
我知道如果你是一个有经验的C程序员,这是个很基础的问题。不过也许也有人没有用过pringf函数。如果你想了解更多的字符,那就买本书或者查阅MSDN。
glPrint("Active OpenGL Text With NeHe - %7.2f", cnt1); // 输出文字到屏幕
最后一件事就是以不同的速率增加计数器的值来产生颜色脉冲并且移动文字。
cnt1+=0.051f; // 增加计数器值
cnt2+=0.005f; // 增加计数器值
return TRUE; // 继续运行
}
最后,如下所示,就是增加在KillGLWindow()函数中增加KillFont()函数,这很重要,它在我们退出程序之前做清理工作。
KillFont(); // 删除字体
原文及其个版本源代码下载: