这次教程中,我们将创建一些基于2D图像的字体,它们可以缩放平移,但不能旋转,并且总是面向前方,但作为基本的显示来说,我想已经足够了。
或者对于这次教程,你会觉得“在屏幕上显示文字没什么难的”,但是你真正尝试过就会知道,它确实没那么容易。你当然可以把文字写在一个图片上,再把这幅图片载入你的OpenGL程序中,打开混合选项,从而在屏幕上显示出文字。但这种做法非常耗时,而且经常图像会显得模糊。另外,除非你的图像包含一个Alpha通道,否则一旦绘制在屏幕上,那些文字就会不透明(与屏幕中的其他物体混合)。
使用位图字体比起使用图形字体(贴图)看起来不止强100倍,你可以随时改变显示在屏幕上的文字,而且用不着为它们逐个制作贴图。只需要将文字定位,再调用我们即将构建的glPrint()函数就可以在屏幕上显示文字了。
程序运行时效果如下:
下面进入教程:
我们这次将在第01课的基础上修改代码,我会对新增代码一一解释,希望大家能掌握,首先打开myglwidget.h文件,将类声明更改如下:
1 #ifndef MYGLWIDGET_H
2 #define MYGLWIDGET_H
3
4 #include <QWidget>
5 #include <QGLWidget>
6
7 class MyGLWidget : public QGLWidget
8 {
9 Q_OBJECT
10 public:
11 explicit MyGLWidget(QWidget *parent = 0);
12 ~MyGLWidget();
13
14 protected:
15 //对3个纯虚函数的重定义
16 void initializeGL();
17 void resizeGL(int w, int h);
18 void paintGL();
19
20 void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
21
22 private:
23 void buildFont(); //创建字体
24 void killFont(); //删除显示列表
25 void glPrint(const char *fmt, ...); //输出字符串
26
27 private:
28 bool fullscreen; //是否全屏显示
29 HDC m_HDC; //储存当前设备的指针
30
31 int m_FontSize; //控制字体的大小
32 GLuint m_Base; //储存绘制字体的显示列表的开始位置
33 GLfloat m_Cnt1; //字体移动计数器1
34 GLfloat m_Cnt2; //字体移动计数器2
35 };
36
37 #endif // MYGLWIDGET_H
我们新增了几个变量,第一个变量m_HDC是用来储存当前设备绘制信息的一种windows数据结构,我们将会把我们自己创建的字体绑定到m_HDC上去,这样我们绘制文字时就自动采用绑定的字体了。后面几个变量的作用依次是控制字体大小、储存绘制字体的显示列表的开始位置、字体移动计数,具体的 会在后面讲。
另外我们增加了三个函数,分别用于创建字体、删除显示列表、输出特定的字符串,当然最后一个glPrint()函数在前面已经提到,是个很重要的函数。
接下来,我们需要打开myglwidget.cpp,加上声明#include <QTimer>、#include <QtMath>,将构造函数和析构函数修改一下,具体代码如下:
1 MyGLWidget::MyGLWidget(QWidget *parent) :
2 QGLWidget(parent)
3 {
4 fullscreen = false;
5 m_FontSize = -18;
6 m_Cnt1 = 0.0f;
7 m_Cnt2 = 0.0f;
8
9 HWND hWND = (HWND)winId(); //获取当前窗口句柄
10 m_HDC = GetDC(hWND); //通过窗口句柄获得HDC
11
12 QTimer *timer = new QTimer(this); //创建一个定时器
13 //将定时器的计时信号与updateGL()绑定
14 connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
15 timer->start(10); //以10ms为一个计时周期
16 }
1 MyGLWidget::~MyGLWidget()
2 {
3 killFont(); //删除显示列表
4 }
几个普通变量的初始化我不作解释了,我们重点看m_HDC的初始化。我们要如何获得当前窗口的HDC呢?方法是我们先得到当前窗口的句柄(HWND),通过调用函数GetCD(HWND)可以获得HDC。那如何获得HWND呢?Qt中有一个winId()函数可以返回当前窗口的Id(类型为WId),我们把它强制转换为HWND类型就可以了,这样我们就可以初始化关键的m_HDC。
注意一下析构函数,在退出程序之前,我们应该确保我们分配的用于存放显示列表的空间被释放,所以我们在析构函数处调用killFont()函数删除显示列表(具体实现看下面)。
继续,我们要来定义我们新增的三个函数了,这可是重头戏,具体代码如下:
1 void MyGLWidget::buildFont() //创建位图字体
2 {
3 HFONT font; //字体句柄
4 m_Base = glGenLists(96); //创建96个显示列表
5
6 font = CreateFont(m_FontSize, //字体高度
7 0, //字体宽度
8 0, //字体的旋转角度
9 0, //字体底线的旋转角度
10 FW_BOLD, //字体的重量
11 FALSE, //是否斜体
12 FALSE, //是否使用下划线
13 FALSE, //是否使用删除线
14 ANSI_CHARSET, //设置字符集
15 OUT_TT_PRECIS, //输出精度
16 CLIP_DEFAULT_PRECIS, //剪裁精度
17 ANTIALIASED_QUALITY, //输出质量
18 FF_DONTCARE | DEFAULT_PITCH, //Family and Pitch的设置
19 LPCWSTR("Courier New")); //字体名称(电脑中已装的)
20
21 wglUseFontBitmaps(m_HDC, 32, 96, m_Base); //创建96个显示列表,绘制ASCII码为32-128的字符
22 SelectObject(m_HDC, font); //选择字体
23 }
1 void MyGLWidget::killFont() //删除显示列表
2 {
3 glDeleteLists(m_Base, 96); //删除96个显示列表
4 }
1 void MyGLWidget::glPrint(const char *fmt, ...) //自定义输出文字函数
2 {
3 char text[256]; //保存字符串
4 va_list ap; //指向一个变量列表的指针
5
6 if (fmt == NULL) //如果无输入则返回
7 {
8 return;
9 }
10
11 va_start(ap, fmt); //分析可变参数
12 vsprintf(text, fmt, ap); //把参数值写入字符串
13 va_end(ap); //结束分析
14
15 glPushAttrib(GL_LIST_BIT); //把显示列表属性压入属性堆栈
16 glListBase(m_Base - 32); //设置显示列表的基础值
17 glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); //调用显示列表绘制字符串
18 glPopAttrib(); //弹出属性堆栈
19 }
首先是buildFont()函数 。我们先定义了字体句柄变量(HFONT),用来存放我们将要创建和使用的字体。接着我们在定义m_Base的同时使用glGenLists(96)创 建了一组共96个显示列表。然后我们调用Windows的API函数CreateFont()来创建我们自己的字体,前13个参数的意义大家请参考注释,我觉得没必要一个个解释了(有兴趣了解CreateFont各个参数 请点击此处 ),最后一个参数是字体类型,我们可以使用我们电脑已安装的任何字体,在WindowsFonts目录可查看电脑已安装的字体。
然后我们从ASCII码第32个字符(空格)开始建立96个显示列表。如果你愿意,也可以建立所有256个字符,只要确保使用glGenLists建立256个显示列表就可以了。最后我们将font对象指针选入HDC,如此就完成了字体的创建及绑定。
然后是killFont()函数。它很简单,就是调用glDeleteLists()函数从m_Base开始删除96个显示列表。
最后是glPrint()函数。首先第一行我们创建一个大小为256个字符的字符数组,将用来保存我们要输出的字符串。第二行我们创建了一个指向一个变量列表的指针,我们在传递字符串的同时也传递了这个变量列表。然后是排除字符串为空的情况。接着的三行代码将文字中的所有符号转换为它们的字符编号,最终文字和转换的符号被储存在字符串text中。然后我们将GL_LIST_BIT压入属性堆栈,它会防止glListBase影响到我们的程序中的其它显示列表。
glListBase(m_Base-32)是告诉OpenGL去哪找对应字符的显示列表,由于每个字符对应一个显示列表,通过m_Base设置一个起点,OpenGL就知道到哪去找到正确的显示列表。减去32是因为我们没有构造前32个显示列表,那么久跳过它们就好了。于是,我们不得不通过从m_Base的值减去32来让OpenGL知道这一点。
现在OpenGL知道字母的存放位置了,我们就可以让它在屏幕上显示文字了。glCallLists()函数能同时将多个显示列表的内容显示在屏幕上,第一个参数是要显示在屏幕上的字符串长度,第二个参数告诉OpenGL将字符串当作一个无符号数组处理,它们的值都介于0到255之间,第三个参数通过传递text来告诉OpenGL显示的具体内容。最后,我们将GL_LIST_BIT属性弹出堆栈,恢复到我们使用glListBase(m_Base-32)之前的状态。
也许你想知道为什么字符不会彼此重叠堆积在一起。那是因为每个字符的显示列表都知道字符的右边缘在哪里,在写完一个字符后,OpenGL自动移动到刚刚写过的字符的右边,再写下一个字或画下一个物体时就会从最后的位置开始,也就是最后一个字符的右边。
然后我们修改一下initializeGL()函数,不作解释,代码如下:
1 void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
2 {
3 glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
4 glShadeModel(GL_SMOOTH); //启用阴影平滑
5 glClearDepth(1.0); //设置深度缓存
6 glEnable(GL_DEPTH_TEST); //启用深度测试
7 glDepthFunc(GL_LEQUAL); //所作深度测试的类型
8 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
9
10 buildFont(); //创建字体
11 }
还有,我们该进入paintGL()函数了,很简单,难的都过去了,具体代码如下:
1 void MyGLWidget::paintGL() //从这里开始进行所以的绘制
2 {
3 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
4 glLoadIdentity(); //重置当前的模型观察矩阵
5
6 glTranslatef(0.0f, 0.0f, -10.0f); //移入屏幕10.0单位
7 //根据字体位置设置颜色
8 glColor3f(1.0f*float(cos(m_Cnt1)), 1.0f*float(sin(m_Cnt2)),
9 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)));
10 //设置光栅化位置,即字体位置
11 glRasterPos2f(-4.5f+0.5f*float(cos(m_Cnt1)), 1.92f*float(sin(m_Cnt2)));
12 //输出文字到屏幕上
13 glPrint("Active OpenGL Text With NeHe - %7.2f", m_Cnt1);
14 m_Cnt1 += 0.051f; //增加两个计数器的值
15 m_Cnt2 += 0.005f;
16 }
值得注意的是,深入屏幕并不能缩小字体,只会给字体变化移动范围(这一点大家自己改改数据就知道了)。然后字体颜色设置和位置设置我觉得没必要解释了,都是数学的东西,我们主要是为了得到一个变化的效果,并不在乎它是怎么实现的。然后就是调用glPrint()函数输出文字,最后增加两个计数器的值就OK了。
最后就是键盘控制的代码了,大家自己看吧,很简单,具体代码如下:
1 void MyGLWidget::keyPressEvent(QKeyEvent *event)
2 {
3 switch (event->key())
4 {
5 case Qt::Key_F1: //F1为全屏和普通屏的切换键
6 fullscreen = !fullscreen;
7 if (fullscreen)
8 {
9 showFullScreen();
10 }
11 else
12 {
13 showNormal();
14 }
15 updateGL();
16 break;
17 case Qt::Key_Escape: //ESC为退出键
18 close();
19 break;
20 case Qt::Key_PageUp: //PageUp按下字体缩小
21 m_FontSize -= 1;
22 if (m_FontSize < -75)
23 {
24 m_FontSize = -75;
25 }
26 buildFont();
27 break;
28 case Qt::Key_PageDown: //PageDown按下字体放大
29 m_FontSize += 1;
30 if (m_FontSize > -5)
31 {
32 m_FontSize = -5;
33 }
34 buildFont();
35 break;
36 }
37 }
现在就可以运行程序查看效果了!