OpenGL(十八)——Qt OpenGL绘制一个3D世界

一、说明

本篇介绍构建一个3D的世界。

二、简介

加载3D世界,并在其中漫游:
在这一课中,你将学会如何加载3D世界,并在3D世界中漫游。
这一课使用第一课的代码,当然在课程说明中我只介绍改变了代码。

这一课是由Lionel Brits (βtelgeuse)所写的。在本课中我们只对增加的代码做解释。当然只添加课程中所写的代码,程序是不会运行的。
如果您有兴趣知道下面的每一行代码是如何运行的话,请下载完整的源码,并在浏览这一课的同时,对源码进行跟踪。
好了现在欢迎来到名不见经传的第十课。到现在为止,您应该有能力创建一个旋转的立方体或一群星星了,对3D编程也应该有些感觉了吧?
但还是请等一下!不要立马冲动地要开始写个Quake IV,好不好...:)。只靠旋转的立方体还很难来创造一个可以决一死战的酷毙了的对手....:)。
现在这些日子您所需要的是一个大一点的、更复杂些的、动态3D世界,它带有空间的六自由度和花哨的效果如镜像、入口、扭曲等等,
当然还要有更快的帧显示速度。这一课就要解释一个基本的3D世界"结构",以及如何在这个世界里游走。
数据结构
当您想要使用一系列的数字来完美的表达3D环境时,随着环境复杂度的上升,这个工作的难度也会随之上升。
出于这个原因,我们必须将数据归类,使其具有更多的可操作性风格。在程序清单头部出现了sector(区段)的定义。
每个3D世界基本上可以看作是sector(区段)的集合。一个sector(区段)可以是一个房间、一个立方体、或者任意一个闭合的区间。

三、代码

头文件

#include <QObject>
#include <QWidget>
#include <qgl.h>
#include <QTextStream>
#include <QKeyEvent>
#include <GL/glu.h>
#include <QMessageBox>
#include <QApplication>#include <math.h>class NeHe_10_1_Widget : public QGLWidget
{Q_OBJECT
public://三角形本质上是由一些(两个以上)顶点组成的多边形,顶点同时也是我们的最基本的分类单位。//顶点包含了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;// 命名为 TRIANGLEtypedef struct tagSECTOR                      // 创建Sector区段结构{int numtriangles;                       // Sector中的三角形个数TRIANGLE* triangle;                     // 指向三角数组的指针} SECTOR;                               // 命名为SECTORNeHe_10_1_Widget(QWidget *parent = 0);~NeHe_10_1_Widget();protected:void resizeGL(int w, int h);void initializeGL();void paintGL();void keyPressEvent(QKeyEvent *event);
private:void setupWorld();void readStr(QTextStream *stream, QString &string);void loadTexture();
private:bool m_show_full_screen;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];bool m_blend;
};

cpp文件:

const float piover180 = 0.0174532925f;NeHe_10_1_Widget::NeHe_10_1_Widget(QWidget *parent) :QGLWidget(parent), m_show_full_screen(false), 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),m_blend(false)
{showNormal();
}NeHe_10_1_Widget::~NeHe_10_1_Widget()
{
}void NeHe_10_1_Widget::resizeGL(int w, int h)
{if(h == 0)// 防止被零除{h = 1;// 将高设为1}glViewport(0, 0, w, h); //重置当前的视口//下面几行为透视图设置屏幕。意味着越远的东西看起来越小。这么做创建了一个现实外观的场景。//此处透视按照基于窗口宽度和高度的45度视角来计算。0.1f,100.0f是我们在场景中所能绘制深度的起点和终点。//glMatrixMode(GL_PROJECTION)指明接下来的两行代码将影响projection matrix(投影矩阵)。//投影矩阵负责为我们的场景增加透视。 glLoadIdentity()近似于重置。它将所选的矩阵状态恢复成其原始状态。//调用glLoadIdentity()之后我们为场景设置透视图。//glMatrixMode(GL_MODELVIEW)指明任何新的变换将会影响 modelview matrix(模型观察矩阵)。//模型观察矩阵中存放了我们的物体讯息。最后我们重置模型观察矩阵。如果您还不能理解这些术语的含义,请别着急。//在以后的教程里,我会向大家解释。只要知道如果您想获得一个精彩的透视场景的话,必须这么做。glMatrixMode(GL_PROJECTION);// 选择投影矩阵glLoadIdentity();// 重置投影矩阵//设置视口的大小gluPerspective(45.0f,(GLfloat)w/(GLfloat)h,0.1f,100.0f);glMatrixMode(GL_MODELVIEW);  //选择模型观察矩阵glLoadIdentity(); // 重置模型观察矩阵
}void NeHe_10_1_Widget::initializeGL()
{loadTexture();glEnable(GL_TEXTURE_2D);                         // Enable Texture MappingglBlendFunc(GL_SRC_ALPHA,GL_ONE);                  // Set The Blending Function For TranslucencyglClearColor(0.0f, 0.0f, 0.0f, 0.0f);              // This Will Clear The Background Color To BlackglClearDepth(1.0);                                  // Enables Clearing Of The Depth BufferglDepthFunc(GL_LESS);                                // The Type Of Depth Test To DoglEnable(GL_DEPTH_TEST);                         // Enables Depth TestingglShadeModel(GL_SMOOTH);                            // Enables Smooth Color ShadingglHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);   // Really Nice Perspective CalculationssetupWorld();m_blend=!m_blend;if (!m_blend){glDisable(GL_BLEND);glEnable(GL_DEPTH_TEST);}else{glEnable(GL_BLEND);glDisable(GL_DEPTH_TEST);}
}void NeHe_10_1_Widget::paintGL()
{glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);        // 清除 场景 和 深度缓冲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);                  // 开始绘制三角形glNormal3f( 0.0f, 0.0f, 1.0f);            // 指向前面的法线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();                        // 三角形绘制结束}
}

显示世界
现在区段已经载入内存,我们下一步要在屏幕上显示它。到目前为止,我们所作过的都是些简单的旋转和平移。
但我们的镜头始终位于原点(0,0,0)处。任何一个不错的3D引擎都会允许用户在这个世界中游走和遍历,我们的这个也一样。
实现这个功能的一种途径是直接移动镜头并绘制以镜头为中心的3D环境。这样做会很慢并且不易用代码实现。我们的解决方法如下:
根据用户的指令旋转并变换镜头位置。
围绕原点,以与镜头相反的旋转方向来旋转世界。(让人产生镜头旋转的错觉)
以与镜头平移方式相反的方式来平移世界(让人产生镜头移动的错觉)。


void NeHe_10_1_Widget::keyPressEvent(QKeyEvent *event)
{switch(event->key()){case Qt::Key_F2:{m_show_full_screen = !m_show_full_screen;if(m_show_full_screen){showFullScreen();}else{showNormal();}updateGL();break;}case Qt::Key_Escape:{qApp->exit();break;}case Qt::Key_PageUp:{m_lookupdown-=1.0f;updateGL();break;}case Qt::Key_PageDown:{m_lookupdown+=1.0f;updateGL();break;}case Qt::Key_Right:{m_heading -=1.0f;m_yrot = m_heading;                           // 向左旋转场景updateGL();break;}case Qt::Key_Left:{m_heading += 1.0f;m_yrot = m_heading;                          // 向右侧旋转场景updateGL();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;        // 使游戏者产生跳跃感updateGL();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;       // 使游戏者产生跳跃感updateGL();break;}//这个实现很简单。当左右方向键按下后,旋转变量yrot//相应增加或减少。当前后方向键按下后,我们使用sine和cosine函数重新生成镜头位置(您需要些许三角函数学的知识:-)。Piover180//是一个很简单的折算因子用来折算度和弧度。//接着您可能会问:walkbias是什么意思?这是NeHe的发明的单词:-)。基本上就是当人行走时头部产生上下摆动的幅度。//我们使用简单的sine正弦波来调节镜头的Y轴位置。如果不添加这个而只是前后移动的话,程序看起来就没这么棒了。case Qt::Key_B:{m_blend=!m_blend;if (!m_blend){glDisable(GL_BLEND);glEnable(GL_DEPTH_TEST);}else{glEnable(GL_BLEND);glDisable(GL_DEPTH_TEST);}updateGL();break;}case Qt::Key_F:{m_filter+=1;if(m_filter > 2){m_filter = 0;}updateGL();}}
}

载入文件
在程序内部直接存储数据会让程序显得太过死板和无趣。从磁盘上载入世界资料,会给我们带来更多的弹性,可以让我们体验不同的世界,
而不用被迫重新编译程序。另一个好处就是用户可以切换世界资料并修改它们而无需知道程序如何读入输出这些资料的。
数据文件的类型我们准备使用文本格式。这样编辑起来更容易,写的代码也更少。等将来我们也许会使用二进制文件。
问题是,怎样才能从文件中取得数据资料呢?首先,创建一个叫做SetupWorld()的新函数。把这个文件定义为filein,并且使用只读方式打开文件。
我们必须在使用完毕之后关闭文件。大家一起来看看现在的代码:


void NeHe_10_1_Widget::setupWorld()
{QFile file(":/world/World.txt");if(!file.open(QIODevice::ReadOnly)){QMessageBox::warning(this, tr("Warning"), tr("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=xm_sector1.triangle[triloop].vertex[vertloop].y = y;   // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值 y=ym_sector1.triangle[triloop].vertex[vertloop].z = z;   // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值 z=zm_sector1.triangle[triloop].vertex[vertloop].u = u;   // 区段 1,  第 triloop 个三角形, 第  vertloop 个顶点, 值 u=um_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 V3file.close();
}//将每个单独的文本行读入变量。这有很多办法可以做到。一个问题是文件中并不是所有的行都包含有意义的信息。
//空行和注释不应该被读入。我们创建了一个叫做readstr()的函数。这个函数会从数据文件中读入一个有意义的行至一个已经初始化过的字符串。
//下面就是代码:
void NeHe_10_1_Widget::readStr(QTextStream *stream, QString &string)
{do                             // 循环开始{string = stream->readLine();} while (string[0] == '/' || string[0] == '\n' || string.isEmpty());        // 考察是否有必要进行处理
}void NeHe_10_1_Widget::loadTexture()
{QImage image(":/data/Crate1.bmp");image = image.convertToFormat(QImage::Format_RGB888);image = image.mirrored();glGenTextures(1, &m_texture[0]);// 创建纹理// Create Nearest Filtered TextureglBindTexture(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());// Create Linear Filtered TextureglBindTexture(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());// Create MipMapped TextureglBindTexture(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());
}

四、运行

写完代码后,我们编译运行一下,看一下运行的效果。

1、运行

2、融合

3、纹理滤波

4、左右旋转

5、跳跃感

6、上下旋转

五、资料

通过上面的结果展示,我们可以看到3D的效果,文章所需的资源,我放到了百度网盘,如果链接失效,大家可以私信我。

链接:https://pan.baidu.com/s/1ocnSrUr161AjFXBgFK7ypQ 
提取码:hak9

上一篇:OpenGL(十七)——Qt OpenGL在三维空间移动位图(会动的星星)

下一篇:OpenGL(十九)——Qt OpenGL波动纹理(旗子的飘动效果)

本文原创作者:冯一川(ifeng12358@163.com),未经作者授权同意,请勿转载。

OpenGL(十八)——Qt OpenGL绘制一个3D世界相关推荐

  1. Qt OpenGL(三十六)——Qt OpenGL 核心模式-绘制雷达坐标系

    提示:本系列文章的索引目录在下面文章的链接里(点击下面可以跳转查看): Qt OpenGL 核心模式版本文章目录 Qt OpenGL(三十六)--Qt OpenGL 核心模式-绘制雷达坐标系 一.场景 ...

  2. 【OpenGL ES】入门及绘制一个三角形

    本文首发于个人博客:Lam's Blog - [OpenGL ES]入门及绘制一个三角形,文章由MarkDown语法编写,可能不同平台渲染效果不一,如果有存在排版错误图片无法显示等问题,烦请移至个人博 ...

  3. Qt OpenGL(二十)——Qt OpenGL 核心模式版本

    Qt OpenGL(二十)--Qt OpenGL 核心模式版本 一.写在前面 在之前的OpenGL教程(1~19)中,采用的方式都是固定渲染管线,也就是OpenGL3.2版本之前的写法,但是OpenG ...

  4. 【OpenGL】十八、OpenGL 绘制多边形 ( 绘制 GL_POLYGON 模式多边形 )

    文章目录 一.绘制 GL_POLYGON 模式多边形 二.多边形绘制顺序分析 三.相关资源 一.绘制 GL_POLYGON 模式多边形 使用 glBegin(GL_POLYGON) 设置绘制多边形 , ...

  5. Android OpenGL ES(十一)绘制一个20面体 .

    前面介绍了OpenGL ES所有能够绘制的基本图形,点,线段和三角形.其它所有复杂的2D或3D图形都是由这些基本图形构成. 本例介绍如何使用三角形构造一个正20面体.一个正20面体,有12个顶点,20 ...

  6. Android OpenGL ES(十一):绘制一个20面体

    前面介绍了OpenGL ES所有能够绘制的基本图形,点,线段和三角形.其它所有复杂的2D或3D图形都是由这些基本图形构成. 本例介绍如何使用三角形构造一个正20面体.一个正20面体,有12个顶点,20 ...

  7. ArcGlobe三维开发之十八——纵断面图的绘制

    我是采用Dev控件中的绘图控件来绘制的纵断面图,下面主要来介绍下输电线路选址以及纵断面图的实时绘制的实现流程. 一.关于输电线路的选址,首先要准备好基础地理数据,包括选线区的DOM,DEM,DLG以及 ...

  8. [pyqtgraph]使用python的pyqtgraph库绘制一个3D图,3张图片按层叠加

    1.首先必找三张尺寸大小一致的图片 # -*- coding: utf-8 -*- """ Use GLImageItem to display image data o ...

  9. OpenGL(十九)——Qt OpenGL波动纹理(旗子的飘动效果)

    OpenGL(十九)--Qt OpenGL波动纹理(旗子的飘动效果) 一.场景 在日常的项目中,我们经常会实现波动的一些纹理效果,比如飘动的旗子,水的波纹,地图上某一点的波浪圈圈等...,本篇介绍波动 ...

最新文章

  1. python自带的shell是什么-python的shell是什么
  2. ZOJ-2571 Big String Outspread 模拟
  3. 微软块级备份引擎服务器,文件级与块级备份区别
  4. Android 数据存储之文件存储小记
  5. 动点四边形周长最短_初中几何--线段之和最小值 Part 1:通过点关于直线对称点得到两定点之间直线段长度最短。...
  6. 实用必备xp框架模块_Xposed框架安装、使用以及插件开发
  7. springboot基于mybatis扫描jar包中的controller、service、dao、xml
  8. 在线JSON转Mongoose工具
  9. CO2 convex sets
  10. X5045的C语言源码,X5045看门狗的单片机源程序和Proteus仿真原理图
  11. FragmentTransactionExtended
  12. python中的对数_python中计算log对数的方法
  13. C++如何限制模板类的类型
  14. 窗帘盒有哪些种类,怎样安装窗帘盒?
  15. 【知识图谱】构建射雕三部曲人物关系
  16. css米奇,屹立48年不倒的IP,机械姬为什么能火这么多年?
  17. python实现矩阵共轭和共轭转置
  18. mysql fabric搭建_MySQL Fabric 安装部署
  19. 计算机组成原理、微机接口及应用综合实验台
  20. 第三方直播云平台(保利威和阿里云)直播集成demo

热门文章

  1. python教程培训福永_在玩耍中学习英语-杜丫丫AI智能英语早教机体验分享
  2. Ajax的简单使用:获取不断刷新的系统时间
  3. Excel 2010 VBA 入门 034 创建图片批注
  4. 通过前后端交互实现简单注册登录功能
  5. php循环经典用法,php的for循环用法详解
  6. /etc/fstab 文件详解
  7. 打开Java美颜丨程序员如何帮女朋友完成整容级修图!
  8. 实验室清洗玻璃仪器小窍门,一篇管够!
  9. ie 检查java_如何判断当前是否为IE浏览器|检查IE浏览器版本信息方法
  10. PLSQL - Months_Between的理解和使用