这次教程中,我将教大家如何加载一个3D世界,并在3D世界中漫游。这相较于我们只能创造一个旋转的立方体或一群星星时有很大的进步了,当然这节课代码难度不低,但也不会很难,只要你跟着我慢慢一步一步来。
一个3D世界当然不像我们之前那样,只要几个对象就搞定了,因此,我们会选择将3D环境用数据来表达,并存放在一个文本中。随着环境复杂度的上升,这个工作得难度也会随之上升。出于这个原因,我们必须将数据归类,使其具有更多的可操作性风格。后面程序中,我们会把3D世界看作是区段(sector)的集合。一个区段可以是一个房间、一个立方体或者任意一个闭合的空间。
程序运行时效果如下:
下面进入教程:
我们这次将在第01课的基础上修改代码,其中一些与前几课重复的地方我不作过多解释。首先打开myglwidget.h文件,将类声明更改如下:
1 #ifndef MYGLWIDGET_H
2 #define MYGLWIDGET_H
3
4 #include <QWidget>
5 #include <QGLWidget>
6
7 typedef struct tagVERTEX //创建Vertex顶点结构体
8 {
9 float x, y, z; //3D坐标
10 float u, v; //纹理坐标
11 } VERTEX;
12
13 typedef struct tagTRIANGLE //创建Triangle三角形结构体
14 {
15 VERTEX vertexs[3]; //3个顶点构成一个Triangle
16 } TRIANGLE;
17
18 typedef struct tagSECTOR //创建Sector区段结构体
19 {
20 int numtriangles; //Sector中的三角形个数
21 QVector<TRIANGLE> vTriangle; //储存三角形的向量
22 } SECTOR;
23
24 class MyGLWidget : public QGLWidget
25 {
26 Q_OBJECT
27 public:
28 explicit MyGLWidget(QWidget *parent = 0);
29 ~MyGLWidget();
30
31 protected:
32 //对3个纯虚函数的重定义
33 void initializeGL();
34 void resizeGL(int w, int h);
35 void paintGL();
36
37 void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
38
39 private:
40 bool fullscreen; //是否全屏显示
41
42 QString m_FileName; //图片的路径及文件名
43 GLuint m_Texture; //储存一个纹理
44 QString m_WorldFile; //存放世界的路径及文本名
45 SECTOR m_Sector; //储存一个区段的数据
46
47 static const float m_PIOVER180 = 0.0174532925f; //实现度和弧度直接的折算
48 GLfloat m_xPos; //储存当前位置
49 GLfloat m_zPos;
50 GLfloat m_yRot; //视角的旋转
51 GLfloat m_LookUpDown; //记录抬头和低头
52 };
53
54 #endif // MYGLWIDGET_H
可以看到我们定义了3个结构体,依次表示顶点,三角形和区段。一个区段包含一系列的多边形(三角形),三角形本质上是由三个以上顶点组合的图形,顶点就是我们最基本的分类单位了。顶点包含了OpenGL真正感兴趣的数据,我们用3D空间中的坐标值(x, y, z)以及它们的纹理坐标(u, v)来定义三角形的每个顶点。这次教程中,我们只加载了一个区段的数据,故只需一个m_Sector数据就够了(当然有兴趣的可以自己设计区段数据,多加载几个看看)。
其他增加的变量,m_PIOVER180就是一个度数和弧度制的折算因子,m_xPos、m_zPos用于记录游戏者的位置,m_yRot用于记录游戏者视角的旋转,m_LookUpDown用于控制游戏者的仰视俯视,简单点说就是抬头低头啦。
接下来,我们需要打开myglwidget.cpp,加上声明#include <QTimer>、#include <QTextStream>、#include <QtMath>,在构造函数中对数据进行初始化,具体代码如下:
1 MyGLWidget::MyGLWidget(QWidget *parent) :
2 QGLWidget(parent)
3 {
4 fullscreen = false;
5 m_FileName = "D:/QtOpenGL/QtImage/Mud.bmp"; //应根据实际存放图片的路径进行修改
6 m_WorldFile = "D:/QtOpenGL/QtImage/World.txt";
7 m_Sector.numtriangles = 0;
8
9 QFile file(m_WorldFile);
10 file.open(QIODevice::ReadOnly | QIODevice::Text); //将要读入数据的文本打开
11 QTextStream in(&file);
12 while (!in.atEnd())
13 {
14 QString line[3];
15 for (int i=0; i<3; i++) //循环读入3个点数据
16 {
17 do //读入数据并保证数据有效
18 {
19 line[i] = in.readLine();
20 }
21 while (line[i][0] == '/' || line[i] == "");
22 }
23 m_Sector.numtriangles++; //每成功读入3个点构成一个三角形
24 TRIANGLE tempTri;
25 for (int i=0; i<3; i++) //将数据储存于一个三角形中
26 {
27 QTextStream inLine(&line[i]);
28 inLine >> tempTri.vertexs[i].x
29 >> tempTri.vertexs[i].y
30 >> tempTri.vertexs[i].z
31 >> tempTri.vertexs[i].u
32 >> tempTri.vertexs[i].v;
33 }
34 m_Sector.vTriangle.push_back(tempTri); //将三角形放入m_Sector中
35 }
36 file.close();
37
38 m_xPos = 0.0f;
39 m_zPos = 0.0f;
40 m_yRot = 0.0f;
41 m_LookUpDown = 0.0f;
42
43 QTimer *timer = new QTimer(this); //创建一个定时器
44 //将定时器的计时信号与updateGL()绑定
45 connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
46 timer->start(10); //以10ms为一个计时周期
47 }
我们重点解释中间对于m_Sector的初始化,我们先将文件打开,再利用Qt的文本流一行一行的读取(为何一行一行读,大家看下存放数据的文本文件World.txt就知道了)并保证读入的数据是有效的。每当成功读入三行数据时,说明构成了一个三角形,就创建一个三角形来储存这些数据,并在最后把三角形放入m_Sector中,当然要给m_Sector的numtriangles加上一,说明多了一个三角形。最后录完数据后,关上文件。或者你会想如果有效数据行数不是3的倍数怎么办,这个问题其实已经不是我们的问题了,而且提供的数据文本存在问题,因此不必考虑。接着的数据初始化不作解释了。
然后在initializeGL()函数中,请大家修改代码如下(不解释):
1 void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
2 {
3 m_Texture = bindTexture(QPixmap(m_FileName));
4 glEnable(GL_TEXTURE_2D);
5
6 glClearColor(0.0, 0.0, 0.0, 0.0); //黑色背景
7 glShadeModel(GL_SMOOTH); //启用阴影平滑
8 glClearDepth(1.0); //设置深度缓存
9 glEnable(GL_DEPTH_TEST); //启用深度测试
10 glDepthFunc(GL_LEQUAL); //所作深度测试的类型
11 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
12 }
任何一个不错的的3D引擎都会允许用户在这个世界中游走和遍历,我们的这个也一样,实现这个功能当然要通过键盘控制。具体实现的途径有一种是直接移动镜头并绘制以镜头为中心的3D环境,但这样会很慢并且不易用代码实现,我们的解决方法如下:
根据用户的指令旋转并变换视角位置。
围绕原点,以与视角相反的旋转方向来旋转世界(让人产生视角旋转的错觉)。
以与视角平移方向相反的方向来平移世界(让人产生视角移动的错觉)。
这样实现起来就简单多了。下面我们先通过键盘控制,来实现平移并旋转视角。
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_LookUpDown -= 1.0f;
22 if (m_LookUpDown < -90.0f)
23 {
24 m_LookUpDown = -90.0f;
25 }
26 break;
27 case Qt::Key_PageDown: //按下PageDown视角向下转
28 m_LookUpDown += 1.0f;
29 if (m_LookUpDown > 90.0f)
30 {
31 m_LookUpDown = 90.0f;
32 }
33 break;
34 case Qt::Key_Right: //Right按下向左旋转场景
35 m_yRot -= 1.0f;
36 break;
37 case Qt::Key_Left: //Left按下向右旋转场景
38 m_yRot += 1.0f;
39 break;
40 case Qt::Key_Up: //Up按下向前移动
41 //向前移动分到x、z上的分量
42 m_xPos -= (float)sin(m_yRot * m_PIOVER180) * 0.05f;
43 m_zPos -= (float)cos(m_yRot * m_PIOVER180) * 0.05f;
44 break;
45 case Qt::Key_Down: //Down按下向后移动
46 //向后移动分到x、z上的分量
47 m_xPos += (float)sin(m_yRot * m_PIOVER180) * 0.05f;
48 m_zPos += (float)cos(m_yRot * m_PIOVER180) * 0.05f;
49 break;
50 }
51 }
这个实现很简单。当左右方向键按下后,旋转变量m_yRot相应的增加或减少。当前后方向键按下时,我们使用sin()和cos()函数计算具体在x和z轴方向上的位移量,使得游戏者能准确的移动。
现在我们已经具备了一切所需的数据,可以开始进行步骤2和3了,当然我们也将进入重点的paintGL()函数。虽然重点,但代码并不难,具体代码如下:
1 void MyGLWidget::paintGL() //从这里开始进行所以的绘制
2 {
3 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
4 glLoadIdentity(); //重置当前的模型观察矩阵
5
6 GLfloat x_m, y_m, z_m, u_m, v_m; //顶点的临时x、y、z、u、v值
7 GLfloat xTrans = -m_xPos; //游戏者沿x轴平移时的大小
8 GLfloat zTrans = -m_zPos; //游戏者沿z轴平移时的大小
9 GLfloat yTrans = -0.25f; //游戏者沿y轴略作平移,使视角准确
10 GLfloat sceneroty = 360.0f - m_yRot; //游戏者的旋转
11
12 glRotatef(m_LookUpDown, 1.0f, 0.0f, 0.0f); //抬头低头的旋转
13 glRotatef(sceneroty, 0.0f, 1.0f, 0.0f); //根据游戏者正面所对方向所作的旋转
14 glTranslatef(xTrans, yTrans, zTrans); //以游戏者为中心平移场景
15
16 glBindTexture(GL_TEXTURE_2D, m_Texture); //绑定纹理
17 for (int i=0; i<m_Sector.numtriangles; i++) //遍历所有的三角形
18 {
19 glBegin(GL_TRIANGLES); //开始绘制三角形
20 glNormal3f(0.0f, 0.0f, 1.0f); //指向前面的法线
21 x_m = m_Sector.vTriangle[i].vertexs[0].x;
22 y_m = m_Sector.vTriangle[i].vertexs[0].y;
23 z_m = m_Sector.vTriangle[i].vertexs[0].z;
24 u_m = m_Sector.vTriangle[i].vertexs[0].u;
25 v_m = m_Sector.vTriangle[i].vertexs[0].v;
26 glTexCoord2f(u_m, v_m);
27 glVertex3f(x_m, y_m, z_m);
28
29 x_m = m_Sector.vTriangle[i].vertexs[1].x;
30 y_m = m_Sector.vTriangle[i].vertexs[1].y;
31 z_m = m_Sector.vTriangle[i].vertexs[1].z;
32 u_m = m_Sector.vTriangle[i].vertexs[1].u;
33 v_m = m_Sector.vTriangle[i].vertexs[1].v;
34 glTexCoord2f(u_m, v_m);
35 glVertex3f(x_m, y_m, z_m);
36
37 x_m = m_Sector.vTriangle[i].vertexs[2].x;
38 y_m = m_Sector.vTriangle[i].vertexs[2].y;
39 z_m = m_Sector.vTriangle[i].vertexs[2].z;
40 u_m = m_Sector.vTriangle[i].vertexs[2].u;
41 v_m = m_Sector.vTriangle[i].vertexs[2].v;
42 glTexCoord2f(u_m, v_m);
43 glVertex3f(x_m, y_m, z_m);
44 glEnd(); //三角形绘制结束
45 }
46 }
就正如我们之前步骤2和3所说,我们以相反的方式来平移和旋转场景,使得看上去是视角在平移和旋转,然后绑定纹理并绘制出整个场景就完成了!
现在就可以运行程序查看效果了!