Qt5版NeHe OpenGL教程之九:加载3D世界,并在其中漫游
扫描二维码
随时随地手机看文章
这一课就要解释一个基本的3D世界"结构",以及如何在这个世界里游走。
lesson9.h
#ifndef LESSON9_H #define LESSON9_H #include#include#include#include//三角形本质上是由一些(两个以上)顶点组成的多边形,顶点同时也是我们的最基本的分类单位。 //顶点包含了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#include#include#include#include#includeLesson9::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 %dn", &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,减去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#includeint 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:控制场景的作用角度