• Qt OpenGL 2D图像文字


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

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

    程序运行时效果如下:

     

    下面进入教程:

    由于相较于之前几课字体教程的代码改动较大,我们将直接在第01课的基础上修改代码,我会一一解释新增的代码,首先myglwidget.h文件,将类声明更改如下:

    1. 1 #ifndef MYGLWIDGET_H
    2. 2 #define MYGLWIDGET_H
    3. 3
    4. 4 #include <QWidget>
    5. 5 #include <QGLWidget>
    6. 6
    7. 7 class MyGLWidget : public QGLWidget
    8. 8 {
    9. 9 Q_OBJECT
    10. 10 public:
    11. 11 explicit MyGLWidget(QWidget *parent = 0);
    12. 12 ~MyGLWidget();
    13. 13
    14. 14 protected:
    15. 15 //3个纯虚函数的重定义
    16. 16 void initializeGL();
    17. 17 void resizeGL(int w, int h);
    18. 18 void paintGL();
    19. 19
    20. 20 void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
    21. 21
    22. 22 private:
    23. 23 void buildFont(); //创建字体
    24. 24 void killFont(); //删除显示列表
    25. 25 //输出字符串
    26. 26 void glPrint(GLuint x, GLuint y, const char *string, int set);
    27. 27
    28. 28 private:
    29. 29 bool fullscreen; //是否全屏显示
    30. 30
    31. 31 GLuint m_Base; //储存绘制字体的显示列表的开始位置
    32. 32 GLfloat m_Cnt1; //字体移动计数器1
    33. 33 GLfloat m_Cnt2; //字体移动计数器2
    34. 34
    35. 35 QString m_FileName[2]; //图片的路径及文件名
    36. 36 GLuint m_Texture[2]; //储存两个纹理
    37. 37 };
    38. 38
    39. 39 #endif // MYGLWIDGET_H

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

    本文福利,费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,Qt编程入门,QT信号与槽机制,QT界面开发-图像绘制,QT网络,QT数据库编程,QT项目实战,QT嵌入式开发,Quick模块,面试题等等)↓↓↓↓↓↓见下面↓↓文章底部点击费领取↓↓

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

    CSDN QT技术栈大纲:Qt开发必备技术栈学习路线和资料

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

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

    1. 1 void MyGLWidget::buildFont() //创建位图字体
    2. 2 {
    3. 3 float cx, cy; //储存字符的x、y坐标
    4. 4 m_Base = glGenLists(256); //创建256个显示列表
    5. 5 glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //选择字符纹理
    6. 6
    7. 7 for (int i=0; i<256; i++) //循环256个显示列表
    8. 8 {
    9. 9 cx = float(i%16)/16.0f; //当前字符的x坐标
    10. 10 cy = float(i/16)/16.0f; //当前字符的y坐标
    11. 11
    12. 12 glNewList(m_Base+i, GL_COMPILE); //开始创建显示列表
    13. 13 glBegin(GL_QUADS); //使用四边形显示每一个字符
    14. 14 glTexCoord2f(cx, 1-cy-0.0625f);
    15. 15 glVertex2i(0, 0);
    16. 16 glTexCoord2f(cx+0.0625f, 1-cy-0.0625f);
    17. 17 glVertex2i(16, 0);
    18. 18 glTexCoord2f(cx+0.0625f, 1-cy);
    19. 19 glVertex2i(16, 16);
    20. 20 glTexCoord2f(cx, 1-cy);
    21. 21 glVertex2i(0, 16);
    22. 22 glEnd(); //四边形字符绘制完成
    23. 23 glTranslated(10, 0, 0); //绘制完一个字符,向右平移10个单位
    24. 24 glEndList(); //字符显示列表完成
    25. 25 }
    26. 26 }
    1. 1 void MyGLWidget::killFont() //删除显示列表
    2. 2 {
    3. 3 glDeleteLists(m_Base, 256); //删除256个显示列表
    4. 4 }
    1. 1 void MyGLWidget::glPrint(GLuint x, GLuint y, //输入字符串
    2. 2 const char *string, int set)
    3. 3 {
    4. 4 if (set > 1) //如果字符集大于1
    5. 5 {
    6. 6 set = 1; //设置其为1
    7. 7 }
    8. 8
    9. 9 glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //绑定为字体纹理
    10. 10 glDisable(GL_DEPTH_TEST); //禁止深度测试
    11. 11 glMatrixMode(GL_PROJECTION); //选择投影矩阵
    12. 12 glPushMatrix(); //保存当前的投影矩阵
    13. 13 glLoadIdentity(); //重置投影矩阵
    14. 14 glOrtho(0, 640, 0, 480, -1, 1); //设置正投影的可视区域
    15. 15 glMatrixMode(GL_MODELVIEW); //选择模型观察矩阵
    16. 16 glPushMatrix(); //保存当前的模型观察矩阵
    17. 17 glLoadIdentity(); //重置模型观察矩阵
    18. 18
    19. 19 glTranslated(x, y ,0); //把字符原点移动到(x,y)位置
    20. 20 glListBase(m_Base-32+(128*set)); //选择字符集
    21. 21 glCallLists(strlen(string), GL_BYTE, string); //把字符串写到屏幕
    22. 22 glMatrixMode(GL_PROJECTION); //选择投影矩阵
    23. 23 glPopMatrix(); //设置为保存的矩阵
    24. 24 glMatrixMode(GL_MODELVIEW); //选择模型观察矩阵
    25. 25 glPopMatrix(); //设置为保存
    26. 26 glEnable(GL_DEPTH_TEST); //启用深度测试
    27. 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. 1 void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
    2. 2 {
    3. 3 m_Texture[0] = bindTexture(QPixmap(m_FileName[0])); //载入位图并转换成纹理
    4. 4 m_Texture[1] = bindTexture(QPixmap(m_FileName[1]));
    5. 5 glEnable(GL_TEXTURE_2D); //启用纹理映射
    6. 6
    7. 7 glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
    8. 8 glShadeModel(GL_SMOOTH); //启用阴影平滑
    9. 9 glClearDepth(1.0); //设置深度缓存
    10. 10 glEnable(GL_DEPTH_TEST); //启用深度测试
    11. 11 glDepthFunc(GL_LEQUAL); //所作深度测试的类型
    12. 12 glBlendFunc(GL_SRC_ALPHA, GL_ONE); //设置混合因子
    13. 13 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
    14. 14
    15. 15 buildFont(); //创建字体
    16. 16 }

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

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

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

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

    本文福利,费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,Qt编程入门,QT信号与槽机制,QT界面开发-图像绘制,QT网络,QT数据库编程,QT项目实战,QT嵌入式开发,Quick模块,面试题等等)↓↓↓↓↓↓见下面↓↓文章底部点击费领取↓↓

  • 相关阅读:
    7.1.7 Java内部类
    ModifyAjaxResponse,修改ajax请求返回值,前后端调试之利器
    【LeetCode刷题笔记】栈和队列
    国自然中标越来越难,怎样才能赢在起跑线上?
    (附源码)ssm旅游企业财务管理系统 毕业设计 102100
    springmvc-day03
    计算机毕业设计之流浪宠物管理系统
    vue2学习之axios在项目中的优化
    如何将音乐导入iphone手机,怎么将歌曲导入到iphone
    合并单元格
  • 原文地址:https://blog.csdn.net/m0_73443478/article/details/128203902