这节课继续上一节课课的内容。在第13课我们学习了如何使用位图字体,这节课,我们将学习如何使用轮廓字体。
创建轮廓字体的方法类似于在第13课中我们创建位图字体的方法。但是,轮廓字体看起来要酷100倍!你可以指定轮廓字体的大小。轮廓字体可以在屏幕中以3D方式运动,而且轮廓字体还可以有一定的厚度!而不是平面的2D字符。使用轮廓字体,你可以将你的计算机中的任何字体转换为OpenGL中的3D字体,加上合适的法线,在有光照的时候,字符就会被很好的照亮了。
一个小注释,这段代码是专门针对Windows写的,它使用了Windows的wgl函数来创建字体,显然,Apple机系统有agl,X系统有glx来支持做同样事情的,不幸的是,我不能保证这些代码也是容易使用的。如果哪位有能在屏幕上显示文字且独立于平台的代码,请告诉我,我将重写一个有关字体的教程。
我们从第一课的典型代码开始,添加上stdio.h头文件以便进行标准输入/输出操作,另外,stdarg.h头文件用来解析文字以及把变量转换为文字。最后加上math.h头文件,这样我们就可以使用SIN和COS函数在屏幕中移动文字了。
另外,我们还要添加2个变量。base将保存我们创建的第一个显示列表的编号。每个字符都需要有自己的显示列表。例如,字符‘A’在显示列表中是65,‘B’是66,‘C’是67,等等。所以,字符‘A’应保存在显示列表中的base + 65这个位置。
我们再添加一个叫做rot的变量。用它配合SIN和COS函数在屏幕上旋转文字。我们同时用它来改变文字的颜色。
GLuint base; // 绘制字体的显示列表的开始位置
GLfloat rot; // 旋转字体
GLYPHMETRICSFLOAT gmf[256]用来保存256个轮廓字体显示列表中对应的每一个列表的位置和方向的信息。我们通过gmf[num]来选择字母。num就是我们想要了解的显示列表的编号。在稍后的代码中,我将说明如何如何检查每个字符的宽度,以便自动将文字定位在屏幕中心。切记,每个字符的宽度可以不相同。Glyphmetrics会大大简化我们的工作。
GLYPHMETRICSFLOAT gmf[256]; // 记录256个字符的信息
下面这段用来构建真正的字体的代码类似于我们创建位图字体的方法。和13课一样,只是使用wglUseFontOutlines函数替换wglUseFontBitmaps函数。
base = glGenLists(256); // 创建256个显示列表
wglUseFontOutlines( hDC, // 设置当前窗口设备描述表的句柄
0, // 用于创建显示列表字体的第一个字符的ASCII值
255, // 字符数
base, // 第一个显示列表的名称
That's not all however. We then set the deviation level. The closer to 0.0f, the smooth the font will look. After we set the deviation, we get to set the font thickness. This describes how thick the font is on the Z axis. 0.0f will produce a flat 2D looking font and 1.0f will produce a font with some depth.
The parameter WGL_FONT_POLYGONS tells OpenGL to create a solid font using polygons. If we use WGL_FONT_LINES instead, the font will be wireframe (made of lines). It's also important to note that if you use GL_FONT_LINES, normals will not be generated so lighting will not work properly.
The last parameter gmf points to the address buffer for the display list data.
0.0f, // 字体的光滑度,越小越光滑,0.0为最光滑的状态
0.2f, // 在z方向突出的距离
WGL_FONT_POLYGONS, // 使用多边形来生成字符,每个顶点具有独立的法线
gmf); //一个接收字形度量数据的数组的地址,每个数组元素用它对应的显示列表字符的数据填充
}
The following code is pretty simple. It deletes the 256 display lists from memory starting at the first list specified by base. I'm not sure if Windows would do this for you, but it's better to be safe than sorry :)
GLvoid KillFont(GLvoid) // 删除显示列表
{
glDeleteLists(base, 256); // 删除256个显示列表
}
下面就是我优异的GL文字程序了。你可以通过调用glPrint(“需要写的文字”)来调用这段代码。文字被存储在字符串text[]中。
GLvoid glPrint(const char *fmt, ...) // 自定义GL输出字体函数
{
下面的第一行定义了一个叫做length的变量。我们使用这个变量来查询字符串的长度。第二行创建了一个大小为256个字符的字符数组,里面保存我们想要的文字串。第三行创建了一个指向一个变量列表的指针,我们在传递字符串的同时也传递了这个变量列表。如果我们传递文字时也传递了变量,这个指针将指向它们。
float length=0; // 查询字符串的长度
char text[256]; // 保存我们想要的文字串
va_list ap; // 指向一个变量列表的指针
下面两行代码检查是否有需要显示的内容,如果什么也没有,屏幕上也就什么都没有。
if (fmt == NULL) // 如果无输入则返回
return;
接下来三行代码将文字中的所有符号转换为它们的字符编号。最后,文字和转换的符号被存储在一个叫做“text”的字符串中。以后我会多解释一些有关字符的细节。
va_start(ap, fmt); // 分析可变参数
vsprintf(text, fmt, ap); // 把参数值写入字符串
va_end(ap); // 结束分析
感谢Jim Williams对下面一段代码的建议。以前我是用手工将文字置于中心的,而他的办法要好的多。
我们从一个循环开始,它将逐个检查文本中的字符。我们通过strlen(text)得到文本的长度。设置好了循环以后,我们将通过加上每个字符的长度来增加length的值。当循环结束以后,被保存在length中的值就是整个字符串的长度。所以,如果我们要写的是“hello”,假设每个字符的长度都为10个单位,我们先给length的值加上第一个字母的长度10。然后,我们检查第二个字母的长度,它的长度也是10,所以length就变成10 + 10(20)。当我们检查完所有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++) // 查找整个字符串的长度
{
length+=gmf[text[loop]].gmfCellIncX;
}
最后我们取出计算后得到的length,并把它变成负数(因为我们要将文本从屏幕中心左移从而把整个文本置于屏幕中间)。然后我们把length除以2。我们并不想移动整个文本的长度,只需要一半!
glTranslatef(-length/2,0.0f,0.0f); // 把字符串置于最左边
然后我们将GL_LIST_BIT压入属性堆栈,它会防止glListBase影响到我们的程序中的其它显示列表
glPushAttrib(GL_LIST_BIT); // 把显示列表属性压入属性堆栈
glListBase(base); // 设置显示列表的基础值为0
现在OpenGL知道字符的存放位置了,我们就可以让它在屏幕上显示文字了。GlCallLists会调用多个显示列表从而把整个文字的内容同时显示在屏幕上。
下面的代码做后续工作。首先,它告诉OpenGL我们将要在屏幕上显示出显示列表中的内容。Strlen(text)函数用来计算我们将要显示在屏幕上的文字的长度。然后,OpenGL需要知道我们允许发送给它的列表的最大值。我们依然不能发送长度大于255的字符串。所以我们使用UNSIGNED_BYTE。(用0 - 255来表示我们需要的字符)。最后,我们通过传递字符串文字告诉OpenGL显示什么内容。
也许你想知道为什么字符不会彼此重叠堆积在一起。那时因为每个字符的显示列表都知道字符的右边缘在那里,在写完一个字符后,OpenGL自动移动到刚写过的字符的右边,在写下一个字或画下一个物体时就会从GL移动到的最后的位置开始,也就是最后一个字符的右边。
最后,我们将GL_LIST_BIT属性弹出堆栈,将GL恢复到我们使用glListBase(base)设置base之前的状态。
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); // 调用显示列表绘制字符串
glPopAttrib(); // 弹出属性堆栈
}
下面就是画图的代码了。我们从清除屏幕和深度缓存开始。我们调用glLoadIdentity()来重置所有东西。然后我们将坐标系向屏幕里移动十个单位。轮廓字体在透视图模式下表现非常好。你将文字移入屏幕越深,文字开起来就更小。文字离你越近,它看起来就更大。
也可以使用glScalef(x,y,z)命令来操作轮廓字体。如果你想把字体放大两倍,可以使用glScalef(1.0f,2.0f,1.0f). 2.0f 作用在y轴, 它告诉OpenGL将显示列表的高度绘制为原来的两倍。如果2.0f作用在x轴,那么文本的宽度将变成原来的两倍。
int DrawGLScene(GLvoid) // 此过程中包括所有的绘制代码
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕及深度缓存
glLoadIdentity(); // 重置当前的模型观察矩阵
glTranslatef(0.0f,0.0f,-10.0f); // 移入屏幕一个单位
在向屏幕里移动以后,我们希望文本能旋转起来。下面3行代码用来在3个轴上旋转屏幕。我将rot乘以不同的数,以便每个方向上的旋转速度不同。
glRotatef(rot,1.0f,0.0f,0.0f); // 沿X轴旋转
glRotatef(rot*1.5f,0.0f,1.0f,0.0f); // 沿Y轴旋转
glRotatef(rot*1.4f,0.0f,0.0f,1.0f); // 沿Z轴旋转
下面是令人兴奋的颜色循环了。照常,我们使用唯一递增的变量(rot)。颜色通过使用COS和SIN来循环变化。我将rot除以不同的数,这样每种颜色会以不同的速度递增。最终的效果非常好。
// 根据字体位置设置颜色
glColor3f(1.0f*float(cos(rot/20.0f)),1.0f*float(sin(rot/25.0f)),1.0f-0.5f*float(cos(rot/17.0f)));
我最喜欢的部分,将文字写到屏幕上。我使用同将位图字体写到屏幕上相同的函数。将文字写在屏幕上,所有你要做的就是glPrint(“你想写的文字”)。很简单。
在下面的代码中,我们要写的是NeHe,空格,破折号,空格,然后是rot的值除以50后的结果(为了减慢计数器)。如果这个数大于999.99,左边第四个数将被去掉(我们要求只显示小数点左边3位数字)。只显示小数点右边的两位数字。
glPrint("NeHe - %3.2f",rot/50); // 输出文字到屏幕
然后增大旋转变量从而改变颜色并旋转文字。