Qt下的OpenGL 编程纹理和贴图





二、openGL坐标系

OpenGL使用右手坐标,从左到右,x递增,从下到上,y递增,从远到近,z递增。

OpenGL坐标系可分为:世界坐标系和当前绘图坐标系。


世界坐标系以屏幕中心为原点(0,0,0)。你面对屏幕,你的右边是x正轴,上面是y正轴,屏幕指向你的为z正轴。长度单位这样来定:窗口范围按此单位恰好是(-1,-1)到(1,1)。


当前绘图坐标系是绘制物体时的坐标系。程序刚初始化时,世界坐标系和当前绘图坐标系是重合的。当用glTranslatef(),glScalef(),glRotatef()对当前绘图坐标系进行平移、伸缩、旋转变换之后,世界坐标系和当前绘图坐标系不再重合。改变以后,再用glVertex3f()等绘图函数绘图时,都是在当前绘图坐标系进行绘图,所有的函数参数也都是相对当前绘图坐标系来讲的。



三、绘制一个三角锥和正方体

绘制三角锥的方法就是在空间中连续地绘制四个三角形,最后形成一个封闭的体。

修改voidNeHeWidget::paintGL()。

[cpp] view plain copy

voidNeHeWidget::paintGL()

{

//清除屏幕和深度缓存

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

glLoadIdentity();

//移到屏幕的左半部分,并且将视图推入屏幕背后足够的距离以便我们可以看见全部的场景

glTranslatef(-1.0f,0.0f,-6.0f);

glRotatef(rTri,1.0,0,0);

glBegin(GL_TRIANGLE_STRIP);

glColor3f(1.0,0.0,0.0);

glVertex3f(0.0,1.0,0.0);

glColor3f(0.0,1.0,0.0);

glVertex3f(-1.0,-1.0,1.0);

glColor3f(0.0,0.0,1.0);

glVertex3f(1.0,-1.0,1.0);

glColor3f(0.0,1.0,0.0);

glVertex3f(1.0,-1.0,-1.0);

glColor3f(1.0,0.0,0.0);

glVertex3f(0.0,1.0,0.0);

glColor3f(0.0,1.0,0.0);

glVertex3f(-1.0,-1.0,1.0);

glEnd();

rTri+=2;

}



在写代码之前,最好在草稿纸上画出三角锥的空间模型,算出每个点的坐标。这里,传给glBegin的是GL_TRIANGLE_STRIP ,

意思就是连续地绘制多个三角行,前两个点就和第三个点组成三角形。

所以一共画四个面,定义了5个点,最后;两个点和最开始的两个点是重合的。

因为在定义了每个顶点的颜色,所以最后每个面都有很漂亮的过度色。


我们用同样的思路来绘制一个正方体。这里传给glBegin的参数是GL_QUAD_STRIP,意思就是连续地绘制正方形。


[cpp] view plain copy

glLoadIdentity();

//移到屏幕的右半部分,并且将视图推入屏幕背后足够的距离以便我们可以看见全部的场景

glTranslatef(1.0f,0.0f,-6.0f);

glRotatef(rTri,1.0,0,0);

glBegin(GL_QUAD_STRIP);

//第一面

glColor3f(1.0,0.0,0.0);

glVertex3f(0.0,0.0,0.0);

glVertex3f(0.0,0.0,1.0);

glVertex3f(1.0,0.0,0.0);

glVertex3f(1.0,0.0,1.0);

//第二个面

glColor3f(0.0,1.0,0.0);

glVertex3f(1.0,1.0,0.0);

glVertex3f(1.0,1.0,1.0);

//第三个面

glColor3f(0.0,0.0,1.0);

glVertex3f(0.0,1.0,0.0);

glVertex3f(0.0,1.0,1.0);

//第四个面

glColor3f(1.0,0.0,0.0);

glVertex3f(0.0,0.0,0.0);

glVertex3f(0.0,0.0,1.0);

glEnd();

//第五个和第六个面

glBegin(GL_QUADS);

glColor3f(0.0,0.8,0.8);

glVertex3f(0.0,1.0,1.0);

glVertex3f(0.0,0.0,1.0);

glVertex3f(1.0,0.0,1.0);

glVertex3f(1.0,1.0,1.0);

glVertex3f(0.0,1.0,0.0);

glVertex3f(0.0,0.0,0.0);

glVertex3f(1.0,0.0,0.0);

glVertex3f(1.0,1.0,0.0);

glEnd();





代码比较简单,最后的效果如下图:




注意所有的面都是逆时针次序绘制的。这点十分重要,这个和平面的正反面有关,以后应该会涉及到。所以要么都逆时针,要么都顺时针,但永远不要将两种次序混在一起,除非您有足够的理由必须这么做。



四、纹理映射

OpenGL纹理的使用分三步:将纹理装入内存,将纹理发送给OpenGL管道,给顶点指定纹理坐标.

修改nehewidget.h:

首先加入装载纹理的函数和几个变量:


//加载纹理函数

voidloadGLTextures();

//正方体在三个方向上的旋转

GLfloatxRot,yRot,zRot;

//texture用来存储纹理

GLuinttexture[1];


在构造函数中加入对旋转量的初始化:

xRot=yRot=zRot=0.0;



接下来实现纹理装载函数:


[cpp] view plain copy

voidNeHeWidget::loadGLTextures()

{

QImagetex,buf;

if(!buf.load(":/data/texture.jpg"))

{

//如果载入不成功,自动生成一个128*128的32位色的绿×××片。

qWarning("Couldnotreadp_w_picpathfile!");

QImagedummy(128,128,QImage::Format_RGB32);

dummy.fill(Qt::green);

buf=dummy;

}

//转换成纹理类型

tex=QGLWidget::convertToGLFormat(buf);

//创建纹理

glGenTextures(1,&texture[0]);

//使用来自位图数据生成的典型纹理,将纹理名字texture[0]绑定到纹理目标上

glBindTexture(GL_TEXTURE_2D,texture[0]);

glTexImage2D(GL_TEXTURE_2D,0,3,tex.width(),tex.height(),0,

GL_RGBA,GL_UNSIGNED_BYTE,tex.bits());

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

}




解释几个函数:

函数原型:


voidglGenTextures(GLsizein,GLuint*textures)


参数说明:


n:用来生成纹理的数量

  textures:存储纹理索引的

函数说明:

  glGenTextures函数根据纹理参数返回n个纹理索引。纹理名称集合不必是一个连续的整数集合。(glGenTextures就是用来产生你要操作的纹理对象的索引的,比如你告诉OpenGL,我需要5个纹理对象,它会从没有用到的整数里返回5个给你)。


函数原型:


voidglBindTexture(GLenumtarget,GLuinttexture);


参数说明:


target:纹理被绑定的目标,它只能取值GL_TEXTURE_1D或者GL_TEXTURE_2D;

texture:纹理的名称,并且,该纹理的名称在当前的应用中不能被再次使用。


函数说明:

glBindTexture实际上是改变了OpenGL的这个状态,它告诉OpenGL下面对纹理的任何操作都是对它所绑定的纹理对象的,比如glBindTexture(GL_TEXTURE_2D,1)告诉OpenGL下面代码中对2D纹理的任何设置都是针对索引为1的纹理的。


函数原型:


voidglTexImage2D(GLenumtarget,GLintlevel,GLintcomponents,GLsizeiwidth,glsizeiheight,GLintborder,GLenumformat,GLenumtype,constGLvoid*pixels);


函数说明:

创建一个纹理。以我们使用的那个函数为例,GL_TEXTURE_2D告诉OpenGL此纹理是一个2D纹理。数字零代表图像的详细程度,通常就由它为零去了。数字三是数据的成分数。因为图像是由红色数据,绿色数据,蓝色数据三种组分组成。tex.width()是纹理的宽度。tex.height()是纹理的高度。数字零是边框的值,一般就是零。GL_RGBA告诉OpenGL图像数据由红、绿、蓝三色数据以及alpha通道数据组成,这个是由于QGLWidget类的converToGLFormat()函数的原因。GL_UNSIGNED_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。


插一句QPixmap和QImag的区别:

QPixmap依赖于硬件,QImage不依赖于硬件。QPixmap主要是用于绘图,针对屏幕显示而最佳化设计,QImage主要是为图像I/O、图片访问和像素修改而设计的。当图片小的情况下,直接用QPixmap进行加载,画图时无所谓,当图片大的时候如果直接用QPixmap进行加载,会占很大的内存,一般一张几十K的图片,用QPixmap加载进来会放大很多倍,所以一般图片大的情况下,用QImage进行加载,然后转乘QPixmap用户绘制。QPixmap绘制效果是最好的。


修改paintGL():

在这里向大家推荐一个很有意思的东东,就是SumoPaint,它是Chrome浏览器里的一个绘图应用,用来在Ubuntu中进行图片编辑还是非常不错的,我们可以用它来编辑纹理贴图。

[cpp] view plain copy

voidNeHeWidget::paintGL()

{

//清除屏幕和深度缓存

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

glLoadIdentity();

//移到屏幕的左半部分,并且将视图推入屏幕背后足够的距离以便我们可以看见全部的场景

glTranslatef(0.0f,0.0f,-5.0f);

glRotatef(xRot,1.0,0.0,0.0);

glRotatef(yRot,0.0,1.0,0.0);

glRotatef(zRot,0.0,0.0,1.0);

//选择使用的纹理

glBindTexture(GL_TEXTURE_2D,texture[0]);

glBegin(GL_QUADS);

glTexCoord2f(0.0,0.0);glVertex3f(-1.0,-1.0,1.0);

glTexCoord2f(1.0,0.0);glVertex3f(1.0,-1.0,1.0);

glTexCoord2f(1.0,1.0);glVertex3f(1.0,1.0,1.0);

glTexCoord2f(0.0,1.0);glVertex3f(-1.0,1.0,1.0);

glTexCoord2f(1.0,0.0);glVertex3f(-1.0,-1.0,-1.0);

glTexCoord2f(1.0,1.0);glVertex3f(-1.0,1.0,-1.0);

glTexCoord2f(0.0,1.0);glVertex3f(1.0,1.0,-1.0);

glTexCoord2f(0.0,0.0);glVertex3f(1.0,-1.0,-1.0);

glTexCoord2f(0.0,1.0);glVertex3f(-1.0,1.0,-1.0);

glTexCoord2f(0.0,0.0);glVertex3f(-1.0,1.0,1.0);

glTexCoord2f(1.0,0.0);glVertex3f(1.0,1.0,1.0);

glTexCoord2f(1.0,1.0);glVertex3f(1.0,1.0,-1.0);

glTexCoord2f(1.0,1.0);glVertex3f(-1.0,-1.0,-1.0);

glTexCoord2f(0.0,1.0);glVertex3f(1.0,-1.0,-1.0);

glTexCoord2f(0.0,0.0);glVertex3f(1.0,-1.0,1.0);

glTexCoord2f(1.0,0.0);glVertex3f(-1.0,-1.0,1.0);

glTexCoord2f(1.0,0.0);glVertex3f(1.0,-1.0,-1.0);

glTexCoord2f(1.0,1.0);glVertex3f(1.0,1.0,-1.0);

glTexCoord2f(0.0,1.0);glVertex3f(1.0,1.0,1.0);

glTexCoord2f(0.0,0.0);glVertex3f(1.0,-1.0,1.0);

glTexCoord2f(0.0,0.0);glVertex3f(-1.0,-1.0,-1.0);

glTexCoord2f(1.0,0.0);glVertex3f(-1.0,-1.0,1.0);

glTexCoord2f(1.0,1.0);glVertex3f(-1.0,1.0,1.0);

glTexCoord2f(0.0,1.0);glVertex3f(-1.0,1.0,-1.0);

glEnd();

xRot+=0.3;

yRot+=0.2;

zRot+=0.4;

}




最后,在initializeGL()中加入


loadGLTextures();

glEnable(GL_TEXTURE_2D);



编译,运行!



它确实跑起来了,不过我们忘记了一个东西,就是纹理坐标。

纹理坐标如下图所示:


假设图中的正方向就是我们要将纹理映射上去的物体(地面),那么我们需要按照图中的表示,为每个顶点指定一个纹理坐标,也称之为UV坐标,它的横向为s轴,纵向围t轴,如下图所示。关于st和uv坐标可以参考一些3D图形学相关知识。


在paintGL()中定义顶点的时候,我们只需用glTexCoord2f()将纹理绑定到相应的顶点就可以了