1. 程式人生 > >Qt5版NeHe OpenGL教程之九:載入3D世界,並在其中漫遊

Qt5版NeHe OpenGL教程之九:載入3D世界,並在其中漫遊

這一課就要解釋一個基本的3D世界"結構",以及如何在這個世界裡遊走。

lesson9.h

#ifndef LESSON9_H
#define LESSON9_H

#include <QWindow>
#include <QOpenGLFunctions_1_1>
#include <QKeyEvent>
#include <QTextStream>

//三角形本質上是由一些(兩個以上)頂點組成的多邊形,頂點同時也是我們的最基本的分類單位。
//頂點包含了OpenGL真正感興趣的資料。我們用3D空間中的座標值(x,y,z)以及它們的紋理座標(u,v)來定義三角形的每個頂點。
typedef struct tagVERTEX	// 建立Vertex頂點結構
{
    float x, y, z;			// 3D 座標
    float u, v;				// 紋理座標
} VERTEX;					// 命名為VERTEX
//一個sector(區段)包含了一系列的多邊形,所以下一個目標就是triangle(我們將只用三角形,這樣寫程式碼更容易些)。
typedef struct tagTRIANGLE	// 建立Triangle三角形結構
{
    VERTEX vertex[3];	    // VERTEX向量陣列,大小為3
}TRIANGLE;                  // 命名為 TRIANGLE
typedef struct tagSECTOR	// 建立Sector區段結構
{
    int numtriangles;		// Sector中的三角形個數
    TRIANGLE* triangle;		// 指向三角陣列的指標
} SECTOR;					// 命名為SECTOR

const float piover180 = 0.0174532925f;

class QPainter;
class QOpenGLContext;
class QOpenGLPaintDevice;

class Lesson9 : public QWindow, QOpenGLFunctions_1_1
{
    Q_OBJECT
public:
    explicit Lesson9(QWindow *parent = 0);
    ~Lesson9();

    virtual void render(QPainter *);
    virtual void render();
    virtual void initialize();

public slots:
    void renderNow();

protected:
    void exposeEvent(QExposeEvent *);
    void resizeEvent(QResizeEvent *);
    void keyPressEvent(QKeyEvent *); // 鍵盤事件

private:
    void setupWorld();
    void readStr(QTextStream *stream, QString &string);
    void loadGLTexture();

private:
    QOpenGLContext *m_context;

    SECTOR m_sector1;

    GLfloat m_yrot;
    GLfloat m_xpos;
    GLfloat m_zpos;
    GLfloat m_heading;
    GLfloat m_walkbias;
    GLfloat m_walkbiasangle;
    GLfloat m_lookupdown;

    GLuint	m_filter;
    GLuint	m_texture[3];
};

#endif // LESSON9_H

lessson9.cpp

#include "lesson9.h"

#include <QCoreApplication>
#include <QOpenGLContext>
#include <QOpenGLPaintDevice>
#include <QPainter>
#include <QDebug>
#include <GL/GLU.h>

Lesson9::Lesson9(QWindow *parent) :
    QWindow(parent)
  , m_context(0)
  , m_yrot(0.0f)
  , m_xpos(0.0f)
  , m_zpos(0.0f)
  , m_heading(0.0f)
  , m_walkbias(0.0f)
  , m_walkbiasangle(0.0f)
  , m_lookupdown(0.0f)
  , m_filter(0)
{
    setSurfaceType(QWindow::OpenGLSurface);
}

Lesson9::~Lesson9()
{
    glDeleteTextures(3, &m_texture[0]);
}

void Lesson9::render(QPainter *painter)
{
    Q_UNUSED(painter);
}
//顯示世界
//現在區段已經載入記憶體,我們下一步要在螢幕上顯示它。到目前為止,我們所作過的都是些簡單的旋轉和平移。
//但我們的鏡頭始終位於原點(0,0,0)處。任何一個不錯的3D引擎都會允許使用者在這個世界中游走和遍歷,我們的這個也一樣。
//實現這個功能的一種途徑是直接移動鏡頭並繪製以鏡頭為中心的3D環境。這樣做會很慢並且不易用程式碼實現。我們的解決方法如下:
//圍繞原點,以與鏡頭相反的旋轉方向來旋轉世界。(讓人產生鏡頭旋轉的錯覺)。
//以與鏡頭平移方式相反的方式來平移世界(讓人產生鏡頭移動的錯覺)。
void Lesson9::render()
{
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

    glViewport(0,0,(GLint)width(),(GLint)height()); // 重置當前視口
    glMatrixMode(GL_PROJECTION);                    // 選擇投影矩陣
    glLoadIdentity();                               // 重置投影矩陣為單位矩陣
    gluPerspective(45.0f,(GLdouble)width()/(GLdouble)height(),0.1f,100.0f);

    glMatrixMode(GL_MODELVIEW);// 選擇模型檢視矩陣
    glLoadIdentity();          // 重置模型檢視矩陣為單位矩陣

    GLfloat x_m, y_m, z_m, u_m, v_m;        // 頂點的臨時 X, Y, Z, U 和 V 的數值
    GLfloat xtrans = -m_xpos;				// 用於遊戲者沿X軸平移時的大小
    GLfloat ztrans = -m_zpos;				// 用於遊戲者沿Z軸平移時的大小
    GLfloat ytrans = -m_walkbias-0.25f;		// 用於頭部的上下襬動
    GLfloat sceneroty = 360.0f - m_yrot;	// 位於遊戲者方向的360度角
    int numtriangles;						// 保有三角形數量的整數
    glRotatef(m_lookupdown, 1.0f, 0 ,0);    // 上下旋轉
    glRotatef(sceneroty, 0, 1.0f, 0);		// 左右旋轉
    glTranslatef(xtrans, ytrans, ztrans);	// 以遊戲者為中心的平移場景
    glBindTexture(GL_TEXTURE_2D, m_texture[m_filter]);	   // 根據filter選擇的紋理
    numtriangles = m_sector1.numtriangles;				   // 取得Sector1的三角形數量
    for (int loop_m = 0; loop_m < numtriangles; loop_m++)  // 遍歷所有的三角形
    {
        glBegin(GL_TRIANGLES);					        // 開始繪製三角形
        x_m = m_sector1.triangle[loop_m].vertex[0].x;	// 第一點的 X 分量
        y_m = m_sector1.triangle[loop_m].vertex[0].y;	// 第一點的 Y 分量
        z_m = m_sector1.triangle[loop_m].vertex[0].z;	// 第一點的 Z 分量
        u_m = m_sector1.triangle[loop_m].vertex[0].u;	// 第一點的 U  紋理座標
        v_m = m_sector1.triangle[loop_m].vertex[0].v;	// 第一點的 V  紋理座標

        glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);	// 設定紋理座標和頂點
        x_m = m_sector1.triangle[loop_m].vertex[1].x;	// 第二點的 X 分量
        y_m = m_sector1.triangle[loop_m].vertex[1].y;	// 第二點的 Y 分量
        z_m = m_sector1.triangle[loop_m].vertex[1].z;	// 第二點的 Z 分量
        u_m = m_sector1.triangle[loop_m].vertex[1].u;	// 第二點的 U  紋理座標
        v_m = m_sector1.triangle[loop_m].vertex[1].v;	// 第二點的 V  紋理座標

        glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);	// 設定紋理座標和頂點
        x_m = m_sector1.triangle[loop_m].vertex[2].x;	// 第三點的 X 分量
        y_m = m_sector1.triangle[loop_m].vertex[2].y;	// 第三點的 Y 分量
        z_m = m_sector1.triangle[loop_m].vertex[2].z;	// 第三點的 Z 分量
        u_m = m_sector1.triangle[loop_m].vertex[2].u;	// 第二點的 U  紋理座標
        v_m = m_sector1.triangle[loop_m].vertex[2].v;	// 第二點的 V  紋理座標
        glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);	// 設定紋理座標和頂點
        glEnd();						                // 三角形繪製結束
    }
}

void Lesson9::initialize()
{
    loadGLTexture();                      // 載入紋理
    glEnable(GL_TEXTURE_2D);              // 啟用紋理對映
    glShadeModel(GL_SMOOTH);              // 啟用平滑著色
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 黑色背景
    glClearDepth(1.0f);                   // 設定深度快取
    glEnable(GL_DEPTH_TEST);              // 啟用深度測試
    glDepthFunc(GL_LEQUAL);               // 深度測試型別
    // 接著告訴OpenGL我們希望進行最好的透視修正。這會十分輕微的影響效能。但使得透檢視看起來好一點。
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

    setupWorld();
}

void Lesson9::renderNow()
{
    if (!isExposed())
        return;

    bool needsInitialize = false;

    if (!m_context) {
        m_context = new QOpenGLContext(this);
        m_context->setFormat(requestedFormat());
        m_context->create();

        needsInitialize = true;
    }

    m_context->makeCurrent(this);

    if (needsInitialize) {
        initializeOpenGLFunctions();
        initialize();
    }

    render();

    m_context->swapBuffers(this);
}

//載入檔案
//在程式內部直接儲存資料會讓程式顯得太過死板和無趣。從磁碟上載入世界資料,會給我們帶來更多的彈性,可以讓我們體驗不同的世界,
//而不用被迫重新編譯程式。另一個好處就是使用者可以切換世界資料並修改它們而無需知道程式如何讀入輸出這些資料的。
//資料檔案的型別我們準備使用文字格式。這樣編輯起來更容易,寫的程式碼也更少。等將來我們也許會使用二進位制檔案。
//問題是,怎樣才能從檔案中取得資料資料呢?首先,建立一個叫做SetupWorld()的新函式。把這個檔案定義為file,並且使用只讀方式開啟檔案。
//我們必須在使用完畢之後關閉檔案。大家一起來看看現在的程式碼:
void Lesson9::setupWorld()
{
    QFile file(":/world/World.txt");
    if(!file.open(QIODevice::ReadOnly))
    {
        qDebug()<<"Can't open world file.";
        return;
    }

    QTextStream stream(&file);
    //我們對區段進行初始化,並讀入部分資料
    QString oneline;		// 儲存資料的字串
    int numtriangles;		// 區段的三角形數量
    float x, y, z, u, v;	// 3D 和 紋理座標

    readStr(&stream, oneline); // 讀入一行資料
    sscanf(oneline.toLatin1().data(), "NUMPOLLIES %d\n", &numtriangles); // 讀入三角形數量

    m_sector1.triangle = new TRIANGLE[numtriangles];		 // 為numtriangles個三角形分配記憶體並設定指標
    m_sector1.numtriangles = numtriangles;					 // 定義區段1中的三角形數量
    // 遍歷區段中的每個三角形
    for (int triloop = 0; triloop < numtriangles; triloop++) // 遍歷所有的三角形
    {
        // 遍歷三角形的每個頂點
        for (int vertloop = 0; vertloop < 3; vertloop++)	 // 遍歷所有的頂點
        {
            readStr(&stream, oneline);				         // 讀入一行資料
            // 讀入各自的頂點資料
            sscanf(oneline.toLatin1().data(), "%f %f %f %f %f", &x, &y, &z, &u, &v);
            // 將頂點資料存入各自的頂點
            m_sector1.triangle[triloop].vertex[vertloop].x = x;	// 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值 x=x
            m_sector1.triangle[triloop].vertex[vertloop].y = y;	// 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值 y=y
            m_sector1.triangle[triloop].vertex[vertloop].z = z;	// 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值 z=z
            m_sector1.triangle[triloop].vertex[vertloop].u = u;	// 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值 u=u
            m_sector1.triangle[triloop].vertex[vertloop].v = v;	// 區段 1,  第 triloop 個三角形, 第  vertloop 個頂點, 值 v=v
        }
    }
    //資料檔案中每個三角形都以如下形式宣告:
    //X1 Y1 Z1 U1 V1
    //X2 Y2 Z2 U2 V2
    //X3 Y3 Z3 U3 V3
    file.close();
}

//將每個單獨的文字行讀入變數。這有很多辦法可以做到。一個問題是檔案中並不是所有的行都包含有意義的資訊。
//空行和註釋不應該被讀入。我們建立了一個叫做readstr()的函式。這個函式會從資料檔案中讀入一個有意義的行
//至一個已經初始化過的字串。下面就是程式碼:
void Lesson9::readStr(QTextStream *stream, QString &string)
{
    do
    {
        string = stream->readLine();
    } while (string[0] == '/' || string[0] == '\n' || string.isEmpty());
}

void Lesson9::loadGLTexture()
{
    QImage image(":/image/Crate.bmp");
    image = image.convertToFormat(QImage::Format_RGB888);
    image = image.mirrored();
    glGenTextures(3, &m_texture[0]);// 建立紋理
    // 建立近鄰濾波紋理
    glBindTexture(GL_TEXTURE_2D, m_texture[0]);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
    glTexImage2D(GL_TEXTURE_2D, 0, 3, image.width(), image.height(),
                 0, GL_RGB, GL_UNSIGNED_BYTE, image.bits());

    // 建立線性濾波紋理
    glBindTexture(GL_TEXTURE_2D, m_texture[1]);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D, 0, 3, image.width(), image.height(),
                 0, GL_RGB, GL_UNSIGNED_BYTE, image.bits());

    // 建立MipMapped濾波紋理
    glBindTexture(GL_TEXTURE_2D, m_texture[2]);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST);
    gluBuild2DMipmaps(GL_TEXTURE_2D, 3, image.width(), image.height(),
                      GL_RGB, GL_UNSIGNED_BYTE, image.bits());
}

void Lesson9::exposeEvent(QExposeEvent *event)
{
    Q_UNUSED(event);

    if (isExposed())
    {
        renderNow();
    }
}

void Lesson9::resizeEvent(QResizeEvent *event)
{
    Q_UNUSED(event);

    if (isExposed())
    {
        renderNow();
    }
}
//當左右方向鍵按下後,旋轉變數yrot。
//當前後方向鍵按下後,我們使用sine和cosine函式重新生成鏡頭位置(您需要些許三角函式學的知識)。
//Piover180是一個很簡單的折算因子用來折算度和弧度。
//接著您可能會問:walkbias是什麼意思?這是NeHe的發明的單詞。基本上就是當人行走時頭部產生上下襬動的幅度。
//我們使用簡單的sine正弦波來調節鏡頭的Y軸位置。如果不新增這個而只是前後移動的話,程式看起來就沒這麼棒了。
void Lesson9::keyPressEvent(QKeyEvent *event)
{
    int key=event->key();
    switch(key)
    {
    case Qt::Key_PageUp:     // 向上旋轉場景
    {
        m_lookupdown-=1.0f;
        break;
    }
    case Qt::Key_PageDown:   // 向下旋轉場景
    {
        m_lookupdown+=1.0f;
        break;
    }
    case Qt::Key_Right:
    {
        m_heading -=1.0f;
        m_yrot = m_heading;	 // 向左旋轉場景
        break;
    }
    case Qt::Key_Left:
    {
        m_heading += 1.0f;
        m_yrot = m_heading;	// 向右側旋轉場景
        break;
    }
    case Qt::Key_Up:
    {
        m_xpos -= (float)sin(m_heading*piover180) * 0.05f;	// 沿遊戲者所在的X平面移動
        m_zpos -= (float)cos(m_heading*piover180) * 0.05f;	// 沿遊戲者所在的Z平面移動
        if (m_walkbiasangle >= 359.0f)					    // 如果walkbiasangle大於359度
        {
            m_walkbiasangle = 0.0f;					        // 將walkbiasangle設為0
        }
        else
        {
            m_walkbiasangle+= 10;					        // 如果walkbiasangle < 359,則增加10
        }
        m_walkbias = (float)sin(m_walkbiasangle * piover180)/20.0f; // 使遊戲者產生跳躍感
        break;
    }
    case Qt::Key_Down:
    {
        m_xpos += (float)sin(m_heading*piover180) * 0.05f;	// 沿遊戲者所在的X平面移動
        m_zpos += (float)cos(m_heading*piover180) * 0.05f;	// 沿遊戲者所在的Z平面移動
        if (m_walkbiasangle <= 1.0f)					    // 如果walkbiasangle小於1度
        {
            m_walkbiasangle = 359.0f;					    // 使walkbiasangle等於359
        }
        else
        {
            m_walkbiasangle-= 10;					        // 如果 walkbiasangle > 1,減去10
        }
        m_walkbias = (float)sin(m_walkbiasangle * piover180)/20.0f;	 // 使遊戲者產生跳躍感
        break;
    }
    case Qt::Key_F:
    {
        m_filter+=1;
        if(m_filter > 2)
        {
            m_filter = 0;
        }
    }
    }
    if(key==Qt::Key_F||key==Qt::Key_PageUp||key==Qt::Key_PageDown||key==Qt::Key_Up||key==Qt::Key_Down
            ||key==Qt::Key_Right||key==Qt::Key_Left)
    {
        renderNow();
    }
}

main.cpp

#include <QGuiApplication>
#include <lesson9.h>
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QSurfaceFormat format;
    format.setSamples(16);

    Lesson9 window;
    window.setFormat(format);
    window.resize(640, 480);
    window.show();

    return app.exec();
}

執行效果


按鍵控制

F鍵:切換三種濾波方式

PageUp和PageDown:控制場景的上下角度

方向鍵Up和Down:控制場景的前進和後退

方向鍵Left和Right:控制場景的作用角度