昨天七夕,关于七夕美好的爱情传说源自于浩瀚银河星空,又碰巧最近在学习QtOpenGL实现三维纹理防体重建,突发奇想用Qt实现一个立方体星空模型,并且能随着鼠标操作实现空间自由旋转
核心思想是用到Qt OpenGL模块,将二维图片贴到立方体的六个面,鼠标可以自由旋转立方体,实现三维星空的动态变换,真正做出来后,感觉效果还挺好的,三维立体星空看起来还是很绚丽的,呵呵
下面直接从代码层面分析上述实例,我用的ubuntu-12.04 Qt-4.8.1
GLFrameWork.pro
main.cpp
mainwindow.h
扩展介绍:信号和槽机制是Qt的核心机制,信号和槽是一种高级接口,应用于对象之间的通信,它是Qt的核心特征,也是Qt区别与其它工具包的重要地方,信号和槽是Qt自行定义的一种通信机制,它独立于标准C/C++语言,因此要正确处理信号和槽,必须借助一个成为moc(Meta Object Compiler)的Qt工具,该工具是一个C++预处理程序,它为高层次的事件处理自动生成所需要的附加代码,在我们熟知的很多GUI工具中窗口小部件(widget) 都有一个回调函数用于响应他们能触发的每个动作,这个回调函数通常是一个指向某个函数的指针,但是在Qt中信号和槽取代了这种l凌乱的函数指针,它使得我们编写这些通信程序更为简洁命了,信号和槽能携带任意数量和任意类型的参数,他们是类型完全安全的,不会像回调函数那样产生core dunps。所有从QObject 或其子类(例如QWidget)派生的类都能购包含信号和槽,当对象改变其状态时,信号就由该对象发射(emit)出去,这就是对象所要做的全部事情,他不知道另一端是谁在接收这个信号,这就是真正的信息封装,它确保对象被当作一个真正的软件组件来使用,槽用于接收信号,但他们是普通的对象成员函数,一个槽并不知道是否有任何信号与自己相链接,而且,对象并不了解具体的通信机制。你可以将很多信号与单个槽进行连接,也可将单个信号与很多槽进行连接,甚至将一个信号与另外一个信号连接也是可能的,这时无论第一个信号什么时候发射,系统都会立刻发射第二个信号,总之信号与槽构造类一个强大的部件编程机制。
信号:当某个信号对其客户或者所有者发生的内部状态发生改变,信号被一个对象发射,只有定义过这个信号的类以及其派生类能够发射这个信号,当一个信号被发射时,与其相关联的槽会被立刻执行,就像一个正常的函数调用一样,信号-槽机制完全独立于任何GUI事件循环,只有当所有的槽返回以后发射函数(emit)才返回,如果存在多个槽与某个信号相关联,那么当这个信号被发射时,这些槽会一个接一个地执行,但是它们执行顺序是随机的、不确定的,我们不能人为的指定那个先执行、哪个后执行。信号的声明在头文件中进行的,QT的signals关键字指出进入类信号声明区,随后即可声明自己的信号。
槽:槽是普通的C++成员函数,可以被正常调用,他们唯一的特殊性就是很多信号可以与其关联,当与其关联信号被发射时,这个槽就会被调用。槽可以有参数,但槽的参数不能有缺省值。既然槽是普通成员函数,因此与其他函数一样,他们也有存取权限,槽的存取权限决定类谁能与其相关联,同普通的C++成员函数一样,槽函数也分为三种类型,public slots, private slots, protected slots。public slots:在这个区内声明的槽意味着任何对象都可将信号与之相连,这对于组件编程非常有用,你可以创建彼此互补了解的对象,将它们的信号与槽进行链接以便信息能够正确的传递。protected slots:在这个区内声明的槽意味着当前类以及其子类可以将信号与之相链接,这适用于那些槽,他们是类实现的一部分,但其界面接口却面向外部。private slots:在这个区内声明的槽意味着只有类字节可以将信号与之相连接,这适用于联系非常紧密的类。槽也能够声明为虚函数,这也是非常有用的,槽的声明也是在头文件中进行的。
信号与槽的关联:通过调用QObject对象的connect函数来将某个对象的信号与另外一个对象的槽函数相关联,这样当发射者发射信号时,接收者的槽函数将被调用,该函数定义如下:bool QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member)[static]这个函数作用就是将发射者sender对象中的信号signal与接收者receiver中的member槽函数联系起来,当指定信号signal时必须使用QT的宏SIGNAL(),当指定槽函数时必须使用宏SLOT()。如果发射者与接收者属于同一个对象的话,那么在connect调用中接收者参数可以省略。当信号与槽没必要继续保持关联时,使用disconnect函数来断开链接,其定义如下: bool QObject::disconnect(const QObject *sender, const char *signal, const Object *receiver, const char *member)[static]这个函数可以断开发射者中的信号与接收者中槽函数之间的关联。在disconnect函数中0可以用作一个通配符,分别表示任何信号、任何接收对象、接收对象中的任何槽函数。但是发射者sender不能为0,其他三个参数值可以为0.
元对象编译器moc(mete object compiler)对C++文件中的类声明进行分析并产生用于初始化元对象的C++代码,元对象包含全部信号和槽的名字以及指向这些函数的指针,moc读C++源文件,如果发现有Q_OBJECT宏声明类,它会生成另外一个C++源文件,这个新生成的文件中包含该类的元对象代码,例如,假如我们有一个头文件mysignal.h,在这个文件中包含有信号或者槽的声明,那么在编译之前moc 工具就会根据该文件自动生成一个mysignal.moc.h的C++源文件并将其提交给编译器,类似地,对应与mysignal.cpp文件moc工具自动生辰mysignal.moc.cpp文件提交给编译器,元对象代码是signal/slot机制所必须的,用moc 产生C++源文件必须与类实现一起进行编译和连接,或者用#include语句将其包含到类的源文件中,moc并不扩展#include或者#define宏定义,它只是简单的跳过所遇到的任何预处理指令。
本实例中,信号xRotationChanged(int angle),即就是当angle变化的时候,则信号开始发射给对应的槽,MainWindow类中的受保护成员函数mousePressEvent(QMouseEvent *event)用于处理鼠标按下时的事件响应,mouseMoveEvent(QMouseEvent *event)用于处理鼠标移动时的事件相应,私有成员函数以及参数不能被外部调用,只能内部使用,包括函数normalizeAngle(int *angle)主要用于标准调整鼠标旋转角度,neheWidget, lastPos, xRot, yRot, zRot都是私有参数。
mainwindow.cpp主要对应于mainwindow.h中的定义编写实现具体的函数实体,按动鼠标左键可以拖动立方体进行空间自由旋转,按动鼠标右键自动退出。
该头文件主要用来定义如何调用OpenGL模块实现三维立体渲染。
对具体定义分别介绍:
#include
#include
Q_OBJECT宏作用,只有加入此宏定义,你才能使用QT中的signal和slot机制。
NeHeWidget类的公共成员函数:explicit NeHeWidget(QWidget *parent=0),explicit用于构造函数,用来抑制隐式转换。扩展:widget被创建时都是不可见的,widget中可容纳其它widget,Qt中的widget在用户行为或者状态改变时会emit signal, QWidget类的构造函数需要一个QWidget*指针作为参数,表示其parent widget(默认值为0,即不存在parent widget ),在parent widget被删除时,Qt会自动删除其所有的child widget,Qt中有三种Layout Manager类:QHBoxLayout, QVBoxLayOut, QGridLayOut,基本模式是将widget添加进LayOut,由Layout自动接管widget的尺寸和位置。
nehewidget.cpp
glShadeModel函数,用于控制OpenGL中绘制指定两点间其他点颜色的过渡模式,参数一般为GL_SMOOTH(默认),GL_FLAT,OpenGL默认是将制定的两点颜色进行插值,绘制之间的其他点,如果两点颜色相同,使用两个参数效果相同,如果两点颜色不同,GL_SMOOTH会出现过渡效果,GL_FLAT则只是以指定的某一点的单一色绘制其他的所有点;glClearColor函数来自OPENGL,为颜色缓冲区指定确定的值,指定red,green,blue,alpha(透明)的值,当颜色缓冲区清空时使用,默认值都是0,其取值范围在0~1之间;glClearDepth函数,设置深度缓存的清除值,depth--指定清除深度缓存时使用的深度值,该值在[0,1]之间,如果设定为0.5,那么物体只有像素深度小于0.5的那部分才可见;glDepthFunc(GLenum func)函数,func:指定“目标像素与当前像素在z方向值大小比较”的函数,符合此函数关系的目标像素才进行绘制,否则目标像素不予绘制,该函数只有启用“深度测试时”glEnable(GL_DEPTH_TEST)和glDisable(GL_DEPTH_TEST)时才有效,参数:GL_LEQUAL如果目标像素z值<=当前像素z值,则绘制目标像素;函数glHint(GLenum target,GLenum mod),该函数控制OpenGL在某一方面有解释的余地时,所采取的操作行为,target:指定所控制行为的符号常量,GL_PERSPECTIVE_CORRECTION_HINT指定颜色和纹理坐标的差值质量,如果OpenGL不能有效的支持透视修正参数差值,那么GL_DONT_CARE和CL_FASTEST可以执行颜色、纹理坐标的简单线性差值计算,mode:指定所采取行为的符号常量,GL_NICEST:选择最高质量选项。
纹理装载函数:LoadGLTextures(),QPixmap和QImge的区别:QPixmap依赖于硬件,QImage不依赖于硬件,QPixmap主要用于绘图,针对屏幕显示最佳化而设计,QImage主要是为图像I/O、图片访问和像素修改而设计的,当图片小的情况下直接用QPixmap进行加载,当图片大的时候如果直接用QPixmap进行加载,会占很大的内存,一般一张几十k的图片,用QPixmap加载进来会放大很多倍,所以一般图片大的情况下,用QImage进行加载,然后转乘QPixmap用户绘制,QPixmap绘制效果是最好的;函数 void glGenTextures(GLsizei n, GLuint *textures)参数n用来生成纹理的数量,textures存储纹理索引的,glGenTextures函数根据纹理参数返回n个纹理索引,纹理名称集合不必是一个连续的整数集合,glGneTextures就是用来产生你要操作的纹理对象的索引的,比如你告诉OpenGL,需要5个纹理对象,它会从没有用到的整数里返回5个给你;函数void glBindTexture(GLenum targt, GLuint texture)参数target纹理被绑定的目标,它只能取值GL_TEXTURE_1D 或者GL_TEXTURE_2D,texture纹理名称,并且该纹理名称在当前的应用中不能被再次使用,该函数实际上改变了OpenGL的这个状态,告诉OpenGL下面对纹理的任何操作都是对它所绑定的纹理对象的,比如glBindTexture(GL_TEXTURE_2D,1)告诉OpenGL下面代码中对2D纹理的任何设置都是针对索引为1纹理的;函数void glTexImage2D(GLenum target, GLint level, GLint components, GLsizei wifth, glsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels),该函数用来创建一个纹理,本例中GL_TEXTURE_2D告诉OpenGL此纹理是一个2D纹理,数字零代表图像的详细程度,通常为0,数字3是数据的成分数,因为图像由红绿蓝三色组成,tex.width()是纹理的宽度,tex.height()纹理的高度,数字0是边框值一般为0,GL_RGBA告诉OpenGL图像由宏绿蓝以及alpha通道组成,这是由于QGLWidget类的converToGLFormat()函数原因,GL_UNSIGNES_BYTE表示组成图像数据是无符号字节类型,最后tex.bits()告诉OpenGL纹理数据来源;glTexParameteri()告诉OpenGL在显示图像时,当它比原始纹理放的大(GL_TEXTURE_MAG_FILTER)或比原始纹理缩的小(GL_TEXTURE_MIN_FILTER)时OpenGL采用的滤波方式,通常这两种情况下都采用GL_LINEAR,这使得纹理从很远处到离屏幕很近时都能平滑显示,使用GL_LINEAR需要CPU和显卡做更多运算,如果机器很慢,应该采用GL_NEAREST,过滤的纹理在放大时候,看起来是斑驳的,因此可以结合这两种滤波方式,在近处时使用GL_LINEAR,远处时用GL_NEAREST。
OpenGL坐标系,OpenGL使用右手坐标系,从左到右,x递增,从下到上,y递增,从远到近,z递增,OpenGL坐标系可分为:世界坐标系和当前绘图坐标系,世界坐标系以屏幕原点(0,0,0),长度单位定为:窗口范围按此单位恰好是(-1,-1)到(1,1),当前绘图坐标系是绘制物体时坐标系,程序初始化时,世界坐标系和当前绘图坐标系是重合的,当用glTranslatef(),glScalef(),glRotatef()对当前绘图坐标系进行平移、伸缩、旋转变换后,世界坐标系和当前绘图坐标系不再重合,改变以后,再用glVertex3f()等绘图函数绘图时,都是在当前绘图坐标系进行绘图,所有的函数参数也都是相对当前绘图坐标系来讲的,OpenGL纹理使用分三步:将纹理装入内存,将纹理发给OpenGL管道,给生成的纹理顶点指定纹理坐标,在paintGL()中定义映射目标物体的顶点时候,我们只需要用glTexCoord2f()将纹理绑定到相应的目标顶点就可以了。
假设纹理坐标如图:
要将其映射到下图正方形形状的物体上(地面),那么就需要按照纹理坐标,为正方形每个顶点指定坐标,也称为UV坐标,横向为s轴,纵向为t轴,将纹理与映射目标绑定。
glClear()函数作用是用当前缓冲区清除值,也就是glClearColor或者glClearDepth、glClearIndex、glClearStencil、glClearAccum等函数所指定的值来清除指定的缓冲区,也可以用glDrawBuffer一次清除多个颜色缓存,比如:glClear(GL_COLOR_BUFFER_BIT)表示把整个窗口清除为黑色,glClear()的唯一参数表示需要被清除的缓冲区,像素检验、裁剪检验、抖动和缓存的写屏蔽都会影响glClear的操作,其中,裁剪范围限制了清除的区域,而glClear命令还会忽略alpha函数、融合函数、逻辑操作、模板、纹理映射和Z缓存;glLoadIdentity()这个函数类似于一个复位操作:X坐标、Y坐标、Z坐标均复位,OpenGL屏幕中心位于原点,在适当的位置使用该函数可以复位坐标,否则下一步的坐标操作就基于上一步的坐标了;glTranslatef(x,y,z)移动时候并不是相对屏幕中心移动,而是相对于当前所在屏幕的位置,其作用就是将你汇点坐标的原点在当前原点的基础上平移一个(x,y,z)向量;旋转所用的函数为glRotatef(Angle, Xvector, Yvector, Zvector),它负责让对象绕某个轴旋转,这个函数有很多用处,Angle通常是个变量代表对象转过的角度,Xvector, Yvector, Zvector三个参数共同决定旋转轴的方向,(1,0,0)描述矢量经过X坐标轴的1个单位处并且方向向右,关于旋转方向确定符合右手定则,大拇指为旋转矢量方向;glBegin 和 glEnd为一对,标志着一组OpenGL操作的开始和结束,并且在参数中告诉了OpenGL下面的操作是针对什么图形进行的,GL_QUADS表示四边形;glVertex3f()确定了矩形的顶点坐标。
重置OpenGL窗口大小函数:resizeGL():其中函数gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar)这个函数定义类观察的视景体在世界坐标系中的具体大小,一般aspect应该与窗口的宽高比相同,fovy视野角度,跟照相机原理相似,数值越小相当于将镜头拉的越近,数值越大,镜头越远,镜头的东西就越小,aspect实际窗口的宽高比x/y,zNear表示近处的裁面,zFar表示远处的裁面;glViewport函数主要负责把视景体截取的图像按照怎样的高和宽显示到屏幕上,该函数还可以调整图像分辨率;glMatrixMode()函数其实就是对接下来做什么进行一下声明,参数有3种模式GL_PROJECTION投影,GL_MODELVIEW模型视图,GL_TEXTURE纹理,如果参数是GL_PROJECTION,这个就是投影的意思,就是要对投影进行相关的操作,也就是把物体投影到一个平面上,就像我们照相一样,把3维物体投影到2维平面上,这样接下来的语句跟透视相关的函数,如glFrustum()或者gluPerspective(),在操作投影矩阵以前,需要调用函数glMatrixMode(GL_PROJECTION)将当前矩阵指定为投影矩阵,然后把矩阵设为单位矩阵glLoadIdentity(),然后调用glFrustum()或者gluPerspective(),他们生成的矩阵会与当前的矩阵相乘,生成透视的效果,GL_MODELVIEW是对模型视图矩阵进行操作,前面GL_PROJECTION设置完成后开始画图,需要切换到模型视图矩阵才能正确画图glMatrixMode(GL_MODELVIEW),如果从头到尾都是画3D/2D,只需要初始化设置一次,如果有交替那么就绪要glMatrixMode()切换,这样设置很烦人于是就有类glPushMatrix()保存当前矩阵。
啊哈,利用两天的时间查找和补充资料,终于完成了这篇博客,夜晚浩瀚的星空,世间的一切都显得如此之渺小,人生数十载如白驹过隙,转眼光阴即逝,怎样让人生过得才有意义?唯有珍惜光阴,不虚度年华,不忘最初的梦想,为梦想而坚持奋斗,这样的人生才有意义。脑海中想起范范“最初的梦想”,唯有“不忘初心,方得始终~”共勉!
原文链接:http://blog.csdn.net/wangchuansnnu/article/details/38356137