• Qt OpenGL 2D图像文字


    这次教程中,我们将学会如何使用四边形纹理贴图把文字显示在屏幕上。我们将把256个不同的文字从一个256×256的纹理图像中一个个提取出来,接着创建一个输出函数来创建任意我们希望的文字。

    还记得在第一篇字体教程中我提到使用纹理在屏幕上绘制文字吗?通常当你使用纹理绘制文字时你会调用你最喜欢的图像处理程序,选择一种字体,然后输入你想显示的文字或段落,然后保存下来位图并把它作为纹理读入到你的程序里,问题是这对一个需要很多文字或者文字在不停变化的程序来说,这么做效率并不高。这次教程中我们只使用一个纹理来显示任意256个不同的字符。

    程序运行时效果如下:

    下面进入教程:

    由于相较于之前几课字体教程的代码改动较大,我们将直接在第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     //输出字符串
    26     void glPrint(GLuint x, GLuint y, const char *string, int set);
    27  
    28 private:
    29     bool fullscreen;                                //是否全屏显示
    30  
    31     GLuint m_Base;                                  //储存绘制字体的显示列表的开始位置
    32     GLfloat m_Cnt1;                                 //字体移动计数器1
    33     GLfloat m_Cnt2;                                 //字体移动计数器2
    34  
    35     QString m_FileName[2];                          //图片的路径及文件名
    36     GLuint m_Texture[2];                            //储存两个纹理
    37 };
    38  
    39 #endif // MYGLWIDGET_H

    我们增加了变量m_Base、m_Cnt1、m_Cnt2,函数声明buildFont()、killFont(),这些和之前都讲过的作用都一样就不重复了。而m_FileName和m_Texture变为了长度为2的数组,这是因为程序中我们会用两种不同的图来建立两个不同的纹理。最后是glPrint()函数的声明,注意下它的参数和前几课不同,但作用是相同的,具体的下面会讲到。

    接下来,我们需要打开myglwidget.cpp,加入声明#include <QTimer>、#include<QtMath>,将构造函数和析构函数修改如下(比较简单不具体解释了):

     1 MyGLWidget::MyGLWidget(QWidget *parent) :
     2     QGLWidget(parent)
     3 {
     4     fullscreen = false;
     5     m_Cnt1 = 0.0f;
     6     m_Cnt2 = 0.0f;
     7     m_FileName[0] = "D:/QtOpenGL/QtImage/Font.bmp";        //应根据实际存放图片的路径进行修改
     8     m_FileName[1] = "D:/QtOpenGL/QtImage/Bumps.bmp";
     9  
    10     QTimer *timer = new QTimer(this);                   //创建一个定时器
    11     //将定时器的计时信号与updateGL()绑定
    12     connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
    13     timer->start(10);                                   //以10ms为一个计时周期
    14 }
    1 MyGLWidget::~MyGLWidget()
    2 {
    3     killFont();                                         //删除显示列表
    4 }

    继续,我们要来定义我们增加的三个函数,同样是重头戏,具体代码如下:

     1 void MyGLWidget::buildFont()                            //创建位图字体
     2 {
     3     float cx, cy;                                       //储存字符的x、y坐标
     4     m_Base = glGenLists(256);                           //创建256个显示列表
     5     glBindTexture(GL_TEXTURE_2D, m_Texture[0]);         //选择字符纹理
     6  
     7     for (int i=0; i<256; i++)                           //循环256个显示列表
     8     {
     9         cx = float(i%16)/16.0f;                         //当前字符的x坐标
    10         cy = float(i/16)/16.0f;                         //当前字符的y坐标
    11  
    12         glNewList(m_Base+i, GL_COMPILE);                //开始创建显示列表
    13             glBegin(GL_QUADS);                          //使用四边形显示每一个字符
    14                 glTexCoord2f(cx, 1-cy-0.0625f);
    15                 glVertex2i(0, 0);
    16                 glTexCoord2f(cx+0.0625f, 1-cy-0.0625f);
    17                 glVertex2i(16, 0);
    18                 glTexCoord2f(cx+0.0625f, 1-cy);
    19                 glVertex2i(16, 16);
    20                 glTexCoord2f(cx, 1-cy);
    21                 glVertex2i(0, 16);
    22             glEnd();                                    //四边形字符绘制完成
    23             glTranslated(10, 0, 0);                     //绘制完一个字符,向右平移10个单位
    24         glEndList();                                    //字符显示列表完成
    25     }
    26 }
    1 void MyGLWidget::killFont()                             //删除显示列表
    2 {
    3     glDeleteLists(m_Base, 256);                         //删除256个显示列表
    4 }
     1 void MyGLWidget::glPrint(GLuint x, GLuint y,            //输入字符串
     2                          const char *string, int set)
     3 {
     4     if (set > 1)                                        //如果字符集大于1
     5     {
     6         set = 1;                                        //设置其为1
     7     }
     8  
     9     glBindTexture(GL_TEXTURE_2D, m_Texture[0]);         //绑定为字体纹理
    10     glDisable(GL_DEPTH_TEST);                           //禁止深度测试
    11     glMatrixMode(GL_PROJECTION);                        //选择投影矩阵
    12     glPushMatrix();                                     //保存当前的投影矩阵
    13     glLoadIdentity();                                   //重置投影矩阵
    14     glOrtho(0, 640, 0, 480, -1, 1);                     //设置正投影的可视区域
    15     glMatrixMode(GL_MODELVIEW);                         //选择模型观察矩阵
    16     glPushMatrix();                                     //保存当前的模型观察矩阵
    17     glLoadIdentity();                                   //重置模型观察矩阵
    18  
    19     glTranslated(x, y ,0);                              //把字符原点移动到(x,y)位置
    20     glListBase(m_Base-32+(128*set));                    //选择字符集
    21     glCallLists(strlen(string), GL_BYTE, string);       //把字符串写到屏幕
    22     glMatrixMode(GL_PROJECTION);                         //选择投影矩阵
    23     glPopMatrix();                                      //设置为保存的矩阵
    24     glMatrixMode(GL_MODELVIEW);                        //选择模型观察矩阵
    25     glPopMatrix();                                      //设置为保存
    26     glEnable(GL_DEPTH_TEST);                            //启用深度测试
    27 }

    首先是buildFont()函数。我们先是定义两个临时变量来储存字体纹理中每个字的位置,cx储存水平方向位置,cy储存竖直方向位置。接着我们告诉OpenGL我们要建立256个显示列表,变量m_Base指向第一个显示列表,然后选择我们的字体纹理。现在我们开始循环,来创建所以256个字符,并存在显示列表里。一开始我们计算得到cx、cy,对16取余和除以16是由于一行是16个字符,最后都除以16.0f是按16个字符把纹理宽度高度均为1.0分成16份。

    后面就开始创建显示列表了,绘制四边形对应纹理时,+或-0.0625f是指一个字符的高或宽,还有由于纹理坐标(0, 0)是在左下角,所以glTexCoord2f(x, y)的第二参数是1-cy、1-cy-0.0625而不是cy、cy+0.0625(比如说cx、cy同时为0,那它对应的字符纹理左下角坐标就应是(0.0, 1-0.0f-0.0625f)了,希望大家能明白)。要注意的是,我们使用glVertex2i()而不是glVertex3f(),我们的字体是2D字体,所以不需要z值。因为我们使用的是正交投影,我们不需要移进屏幕,在一个正交投影平面绘图你所需要的是指定x、y坐标。又因为我们的屏幕是以像素形式从0到639(宽),从0到479(高),因此我们既不需要用浮点数也不需要负数。

    画完四边形后,我们右移了10个像素,至于纹理有病。如果我们不平移,文字将会重叠。有由于我们的字体太窄太瘦,我们不想右移16个像素那么多,如果那样的话,每个字符之间将有很大的间隔,只移动10个像素是个不错的选择。

    接着是killFont()函数。它很简单,就是调用glDeleteLists()函数从m_Base开始删除256个显示列表。

    最后是glPrint()函数。首先我们判断一下set字符集,如果大于1,就将set置0。这是由于我们的字体纹理中只有两种字体,第一种是普通的,第二种是斜体,如果选择的字符集有误,我们就把它设为默认的普通字符集。接着我们再次选择字体纹理,我们这么做事防止我们在决定往屏幕上输出文字前选择了别的纹理,导致出错。然后我们禁用了深度测试,我们这么做事因为混合的效果会更好。如果我们不禁用深度测试,文字可能会被什么东西挡住,或者得不到正确的混合效果。当然,如果你不打算混合文字(那样文字周围的黑色区域就不会显示),你就可以启用深度测试。

    下面几行十分重要!我们选择投影矩阵,然后调用glPushMatrix()函数,保存当前投影矩阵(其实就是把投影矩阵压入堆栈)。保存投影矩阵后,我们重置矩阵并调用glOrtho()设置正交投影屏幕,第一和第三个参数表示屏幕的底边和左边,第二和第四个参数表示屏幕的上边和右边。由于我们不需要用到深度测试,所以我们将第五和第六个参数设为-1和1。我们再选择模型观察矩阵,用glPushMatrix()保存当前设置。然后我们重置模型观察矩阵以便在正交投影视点下工作。

    现在我们可以绘制文字了,我们从移动到绘制文字的位置开始。我们使用glTranslated()而不是glTranslatef(),因为我们处理的是像素,所以浮点数没有意义。接着我们用glListBase()来选择字符集,如果我们想使用第二个字符集,我们在当前的显示列表基数上加上128,通过加128,我们跳过了前128个字符。而减去32是因为我们的字符集是从“空格”开始的,即ASCII码第33个字符开始,故我们减去32,告诉OpenGL跳过前面32个字符。然后我们使用glCallLists()绘制文字,这个之前解释过,不再解释了。

    最后我们要做的是恢复透视视图。我们选择投影矩阵并用glPopMatrix()恢复我们先前glPushMatrix()保存的设置,接着选择模型观察矩阵做相同的工作。你或许会问,难道不用按相反顺序弹出矩阵吗?不用,这是用于投影矩阵和模型观察矩阵的堆栈并不是同一个(这样说其实并不准确,不过道理是差不多的),所以无论选择哪个矩阵先弹出都没有问题。值得注意的是,如果你把代码中的最后两句glMatrixMode()调换位置,运行程序时你是看不到图像纹理的只能看到文字,这是由于我们最后选择的矩阵是GL_PROJECTION,而我们绘制图像纹理是在GL_MODEWIEW上绘制的,所以你看不到图像纹理。当然解决办法就是,在恢复了投影矩阵后,开始深度测试之前,再次调用glMatrix()选择模型观察矩阵GL_MODEVIEW。那为什么我们能看到文字呢?这是由于做了平面正交投影后,在任何矩阵所绘制的东西都是在平面绘制的,OpenGL自动会把它们投影到屏幕上,所以总能看到文字的。函数最后我们启用了深度测试,如果你没有在上面的代码中关闭深度测试,就不需要这行。

    然后我们修改一下initializeGL()函数,具体代码如下:

     1 void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
     2 {
     3     m_Texture[0] = bindTexture(QPixmap(m_FileName[0])); //载入位图并转换成纹理
     4     m_Texture[1] = bindTexture(QPixmap(m_FileName[1]));
     5     glEnable(GL_TEXTURE_2D);                            //启用纹理映射
     6  
     7     glClearColor(0.0, 0.0, 0.0, 0.0);                   //黑色背景
     8     glShadeModel(GL_SMOOTH);                            //启用阴影平滑
     9     glClearDepth(1.0);                                  //设置深度缓存
    10     glEnable(GL_DEPTH_TEST);                            //启用深度测试
    11     glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
    12     glBlendFunc(GL_SRC_ALPHA, GL_ONE);                  //设置混合因子
    13     glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
    14  
    15     buildFont();                                        //创建字体
    16 }

    最开始三行载入位图转换纹理,启用纹理映射就不解释了。中间部分有小的改动,由于我们要给字体上色,所以要设置混合因子。最后调用buildFont()创建字体。

    最后,我们该进入paintGL()函数,这次难度还行,具体代码如下:

     1 void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
     2 {
     3     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
     4     glLoadIdentity();                                   //重置当前的模型观察矩阵
     5  
     6     glBindTexture(GL_TEXTURE_2D, m_Texture[1]);         //设置为图像纹理
     7     glTranslatef(0.0f, 0.0f, -5.0f);                    //移入屏幕5.0单位
     8     glRotatef(45.0f, 0.0f, 0.0f, 1.0f);                 //绕z轴旋转45度
     9     glRotatef(m_Cnt1*30.0f, 1.0f, 1.0f, 0.0f);          //绕(1,1,0)轴旋转
    10     glDisable(GL_BLEND);                                //关闭融合
    11     glColor3f(1.0f, 1.0f, 1.0f);                        //设置颜色为白色
    12     glBegin(GL_QUADS);                                  //绘制纹理四边形
    13         glTexCoord2d(0.0f, 0.0f);
    14         glVertex2f(-1.0f, 1.0f);
    15         glTexCoord2d(1.0f, 0.0f);
    16         glVertex2f(1.0f, 1.0f);
    17         glTexCoord2d(1.0f, 1.0f);
    18         glVertex2f(1.0f, -1.0f);
    19         glTexCoord2d(0.0f, 1.0f);
    20         glVertex2f(-1.0f, -1.0f);
    21     glEnd();
    22  
    23     glRotatef(90.0f, 1.0f, 1.0f, 0.0);                  //绕(1,1,0)轴旋转90度
    24     glBegin(GL_QUADS);                                  //绘制第二个四边形,与第一个垂直
    25         glTexCoord2d(0.0f, 0.0f);
    26         glVertex2f(-1.0f, 1.0f);
    27         glTexCoord2d(1.0f, 0.0f);
    28         glVertex2f(1.0f, 1.0f);
    29         glTexCoord2d(1.0f, 1.0f);
    30         glVertex2f(1.0f, -1.0f);
    31         glTexCoord2d(0.0f, 1.0f);
    32         glVertex2f(-1.0f, -1.0f);
    33     glEnd();
    34  
    35     glEnable(GL_BLEND);                                 //启用混合
    36     glLoadIdentity();                                   //重置视口
    37     //根据字体位置设置颜色
    38     glColor3f(1.0f*float(cos(m_Cnt1)), 1.0*float(sin(m_Cnt2)),
    39             1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)));
    40     glPrint(int((280+250*cos(m_Cnt1))),
    41             int(235+200*sin(m_Cnt2)), "NeHe", 0);
    42     glColor3f(1.0*float(sin(m_Cnt2)), 1.0f-0.5f*float(cos(m_Cnt1+m_Cnt2)),
    43               1.0f*float(cos(m_Cnt1)));
    44     glPrint(int((280+230*cos(m_Cnt2))),
    45             int(235+200*sin(m_Cnt1)), "OpenGL", 1);
    46     glColor3f(0.0f, 0.0f, 1.0f);
    47     glPrint(int(240+200*cos((m_Cnt1+m_Cnt2)/5)), 2,
    48             "Giuseppe D'Agata", 0);
    49     glColor3f(1.0f, 1.0f, 1.0f);
    50     glPrint(int(242+200*cos((m_Cnt1+m_Cnt2)/5)), 2,
    51             "Giuseppe D'Agata", 0);
    52  
    53     m_Cnt1 += 0.01f;                                   //增加两个计数器的值
    54     m_Cnt2 += 0.0081f;
    55 }

    函数中我们先绘制3D物体最后绘制文字,这样文字将显示在3D物体上面,而不会被3D物体遮住。我们之所以加入一个3D物体是为了演示透视投影和正交投影可同时使用。首先我们选择纹理,为了看见3D物体,我们往屏幕内移动5个单位。我们绕z轴旋转45度,这将使我们的四边形顺时针旋转45度,让我们的四边形看起来更像砖石而不是矩形,接着我们让物体同时绕x、y轴旋转m_Cnt1*30度,这使我们的物体像在一个点上旋转的钻石那样旋转。然后我们关闭混合,设置颜色为亮白,绘制第一个纹理映射的四边形。再绕x、y轴旋转90度,画另一个四边形,第二个四边形从第一个四边形中间切过去,来形成一个好看的形状。

    在绘制完有纹理贴图的四边形后,我们开启混合并绘制文字,下面的根据文字选择颜色,打印“NeHe”、“OpenGL”就不解释了。我们来看打印“Giuseppe D'Agata”时,我们用深蓝色和亮白色两次绘制(作者的名字),并在x方向上平移2个像素,这样创造出一种亮白色字附带深蓝色阴影的效果,感觉真的很棒啊!要注意的是,这里必须打开混合,如果没有打开是不会出现这样的效果的(大家可以注释掉glEnable(GL_BLEND)看看,我就不解释了),甚至其它两个字符串也变得糟糕透了。最后一件事是以不同的速率递增我们的计数器,这使得文字移动,3D物体自转。

    现在就可以运行程序查看效果了!

  • 相关阅读:
    【Anagrams】 cpp
    【Count and Say】cpp
    【Roman To Integer】cpp
    【Integer To Roman】cpp
    【Valid Number】cpp
    重构之 实体与引用 逻辑实体 逻辑存在的形式 可引用逻辑实体 不可引用逻辑实体 散弹式修改
    Maven项目聚合 jar包锁定 依赖传递 私服
    Oracle学习2 视图 索引 sql编程 游标 存储过程 存储函数 触发器
    mysql案例~tcpdump的使用
    tidb架构~本地化安装
  • 原文地址:https://www.cnblogs.com/ybqjymy/p/14048454.html
Copyright © 2020-2023  润新知