這一課就要解釋一個基本的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:控制場景的作用角度
Qt5版本NeHe OpenGL教程6-10課源碼鏈接:https://download.csdn.net/download/caoshangpa/10420544