Qt OpenGL 載入3D世界,並在其中漫遊
阿新 • • 發佈:2020-11-27
這次教程中,我將教大家如何載入一個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所說,我們以相反的方式來平移和旋轉場景,使得看上去是視角在平移和旋轉,然後繫結紋理並繪製出整個場景就完成了!
現在就可以執行程式檢視效果了!