第23课:球面映射 (参照NeHe)
这次教程中,我们将学会如何把环境纹理包裹在我们的3D模型上,让它看起来像反射了周围的景象一样,我们把这种纹理映射的方式称为球体映射。球体映射是一种创建金属反射效果的方法,虽然它并不像真实世界里那么精确,但效果还是很不错的!
红宝书中,球体映射的定义为,把一幅位于无限远的图像映射到球面上。当然我们需要自己来创建一幅球体环境映射图,方法如下:
打开Photoshop,并在Photoshop中打开我们要转换成球体环境映射图的原图,选择所有的像素点,创建它的一个复制。接着我们把图像变为2的幂次方大小,一般为128×128或256×256.最后使用扭曲滤镜,并应用球体效果,然后把得到的图像保存为 *.bmp文件(PS:我给大家的资源文件中球体映射图是处理好的了,命名为Reflect.bmp)。
程序运行时效果如下:
下面进入教程:
我们这次将在第18课的基础上修改代码,这次相比前两次简单太多了,我只会对代码的变化作出解释。首先打开myglwidget.h文件,将类声明更改如下:
#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H
#include
#include
class GLUquadric;
class MyGLWidget : public QGLWidget
{
Q_OBJECT
public:
explicit MyGLWidget(QWidget *parent = 0);
~MyGLWidget();
protected:
//对3个纯虚函数的重定义
void initializeGL();
void resizeGL(int w, int h);
void paintGL();
void keyPressEvent(QKeyEvent *event); //处理键盘按下事件
private:
void glDrawCube(); //绘制立方体
private:
bool fullscreen; //是否全屏显示
QString m_FileName[2]; //图片的路径及文件名
GLuint m_Texture[2]; //储存两个纹理
bool m_Light; //光源的开/关
GLfloat m_xRot; //x旋转角度
GLfloat m_yRot; //y旋转角度
GLfloat m_xSpeed; //x旋转速度
GLfloat m_ySpeed; //y旋转速度
GLfloat m_Deep; //深入屏幕的距离
GLUquadric *m_Quadratic; //二次几何体
GLuint m_Object; //绘制对象标示符
};
#endif // MYGLWIDGET_H
由于我们程序的背景图和用于纹理映射的图并不是同一张(后者是PS处理过的那张),所以我们需要储存两个不同的纹理,m_FileName和m_Texture就都变成长度为2的数组啦。然后我们删掉了m_Part1、m_Part2、m_P1、m_P2等变量,由于我们后面的绘制过程并不打算绘制圆盘和部分圆盘,这是因为它们绘制出来的映射效果并不好,所以我就不绘制它们了。
接下来,我们需要打开myglwidget.cpp,对构造函数进行修改(更换图片途径名和删掉不存在的变量),很简单不多解释,具体代码如下:
MyGLWidget::MyGLWidget(QWidget *parent) :
QGLWidget(parent)
{
fullscreen = false;
m_FileName[0] = "D:/QtOpenGL/QtImage/BG.bmp"; //应根据实际存放图片的路径进行修改
m_FileName[1] = "D:/QtOpenGL/QtImage/Reflect.bmp";
m_Light = false;
m_xRot = 0.0f;
m_yRot = 0.0f;
m_xSpeed = 0.0f;
m_ySpeed = 0.0f;
m_Deep = -10.0f;
m_Object = 0;
QTimer *timer = new QTimer(this); //创建一个定时器
//将定时器的计时信号与updateGL()绑定
connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
timer->start(10); //以10ms为一个计时周期
}
void MyGLWidget::glDrawCube()
{
glBegin(GL_QUADS); //开始绘制立方体
glNormal3f(0.0f, 0.5f, 0.0f);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(1.0f, 1.0f, -1.0f); //右上(顶面)
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f); //左上(顶面)
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-1.0f, 1.0f, 1.0f); //左下(顶面)
glTexCoord2f(1.0f, 0.0f);
glVertex3f(1.0f, 1.0f, 1.0f); //右下(顶面)
glNormal3f(0.0f, -0.5f, 0.0f);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(1.0f, -1.0f, 1.0f); //右上(底面)
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 1.0f); //左上(底面)
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f); //左下(底面)
glTexCoord2f(0.0f, 1.0f);
glVertex3f(1.0f, -1.0f, -1.0f); //右下(底面)
glNormal3f(0.0f, 0.0f, 0.5f);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 1.0f); //右上(前面)
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f); //左上(前面)
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 1.0f); //左下(前面)
glTexCoord2f(1.0f, 0.0f);
glVertex3f(1.0f, -1.0f, 1.0f); //右下(前面)
glNormal3f(0.0f, 0.0f, -0.5f);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(1.0f, -1.0f, -1.0f); //右上(后面)
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, -1.0f); //左上(后面)
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f); //左下(后面)
glTexCoord2f(0.0f, 1.0f);
glVertex3f(1.0f, 1.0f, -1.0f); //右下(后面)
glNormal3f(-0.5f, 0.0f, 0.0f);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f); //右上(左面)
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f); //左上(左面)
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, -1.0f); //左下(左面)
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 1.0f); //右下(左面)
glNormal3f(0.5f, 0.0f, 0.0f);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(1.0f, 1.0f, -1.0f); //右上(右面)
glTexCoord2f(0.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 1.0f); //左上(右面)
glTexCoord2f(0.0f, 0.0f);
glVertex3f(1.0f, -1.0f, 1.0f); //左下(右面)
glTexCoord2f(1.0f, 0.0f);
glVertex3f(1.0f, -1.0f, -1.0f); //右下(右面)
glEnd(); //立方体绘制结束
}
几乎没有什么改动,就是把法线(glNormal)的范围从[-1, 1]缩放到[-0.5, 0.5]。如果法向量太大的话,会产生一些块状效果,影响视觉效果。
然后我们需要修改一下initializeGL()函数,我们添加一些新的函数(glTexGeni)来使用球体纹理映射,具体代码如下:
void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
{
m_Texture[0] = bindTexture(QPixmap(m_FileName[0])); //载入位图并转换成纹理
m_Texture[1] = bindTexture(QPixmap(m_FileName[1]));
glEnable(GL_TEXTURE_2D); //启用纹理映射
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);//设置s方向的纹理坐标自动生成
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);//设置t方向的纹理坐标自动生成
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //黑色背景
glShadeModel(GL_SMOOTH); //启用阴影平滑
glClearDepth(1.0); //设置深度缓存
glEnable(GL_DEPTH_TEST); //启用深度测试
glDepthFunc(GL_LEQUAL); //所作深度测试的类型
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
m_Quadratic = gluNewQuadric(); //创建二次几何体
gluQuadricNormals(m_Quadratic, GLU_SMOOTH); //使用平滑法线
gluQuadricTexture(m_Quadratic, GL_TRUE); //使用纹理
//光源部分
GLfloat LightAmbient[] = {0.5f, 0.5f, 0.5f, 1.0f}; //环境光参数
GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f}; //漫散光参数
GLfloat LightPosition[] = {0.0f, 0.0f, 2.0f, 1.0f}; //光源位置
glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); //设置环境光
glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); //设置漫射光
glLightfv(GL_LIGHT1, GL_POSITION, LightPosition); //设置光源位置
glEnable(GL_LIGHT1); //启动一号光源
}
注意到,我们除了加载了两个位图并转换成纹理外,我们还调用了glTexGeni()函数。这个函数在之前第15课有提到过,就是让OpenGL为我们在指定的方向上(S、T方向),自动生成纹理映射的坐标。然后函数的第三个参数设置为GL_SPHERE_MAP,使得创建出一种有金属质感的物体(大家不记得这个函数了的话,请参照前面 第15课)。
还有就是paintGL()函数,我们也是作了部分的修改,我只解释增加的部分,具体代码如下:
void MyGLWidget::paintGL() //从这里开始进行所以的绘制
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
glLoadIdentity(); //重置模型观察矩阵
glTranslatef(0.0f, 0.0f, m_Deep); //移入屏幕
glEnable(GL_TEXTURE_GEN_S); //启用自动生成s方向纹理坐标
glEnable(GL_TEXTURE_GEN_T); //启用自动生成t方向纹理坐标
glBindTexture(GL_TEXTURE_2D, m_Texture[1]); //选择球体映射纹理
glPushMatrix(); //保存模型观察矩阵
glRotatef(m_xRot, 1.0f, 0.0f, 0.0f); //绕x轴旋转
glRotatef(m_yRot, 0.0f, 1.0f, 0.0f); //绕y轴旋转
switch(m_Object)
{
case 0: //绘制立方体
glDrawCube();
break;
case 1: //绘制圆柱体
glTranslatef(0.0f, 0.0f, -1.5f);
gluCylinder(m_Quadratic, 1.0f, 1.0f, 3.0f, 64, 64);
break;
case 2: //绘制球
gluSphere(m_Quadratic, 1.3f, 64, 64);
break;
case 3: //绘制圆锥
glTranslatef(0.0f, 0.0f, -1.5f);
gluCylinder(m_Quadratic, 1.0f, 0.0f, 3.0f, 64, 64);
break;
}
glPopMatrix(); //恢复模型观察矩阵
glDisable(GL_TEXTURE_GEN_S); //禁用自动生成纹理坐标
glDisable(GL_TEXTURE_GEN_T);
glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //选择平面纹理
glPushMatrix(); //保存模型观察矩阵
glTranslatef(0.0f, 0.0f, -24.0); //移入屏幕24.0单位
glBegin(GL_QUADS); //绘制背景四边形
glNormal3f(0.0f, 0.0f, 1.0f);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-13.3f, -10.0f, 10.0f);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(13.3f, -10.0f, 10.0f);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(13.3f, 10.0f, 10.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-13.3f, 10.0f, 10.0f);
glEnd();
glPopMatrix(); //恢复模型观察矩阵
m_xRot += m_xSpeed; //x轴旋转
m_yRot += m_ySpeed; //y轴旋转
}
首先在移入屏幕(glTranslate)后,我们开启自动生成纹理坐标,要注意必须自己手动开启,不然前面initializeGL()函数中的设置是不会生效的。接着我们选择球面映射的纹理,保存模型观察矩阵(glPushMatrix)后,进行旋转后绘制会我们的3D模型。绘制完3D模型后,我们恢复模型观察矩阵(glPopMatrix)并禁用自动生成纹理坐标。正如前面提到,由于绘制出来的圆盘和部分圆盘效果并不好,所以我们把绘制它们的部分删掉了,对switch语句作了修改,现在我们将会绘制的3D模型有立方体、圆柱、球和圆锥。
然后我们选择作为背景的平面纹理,同样保存模型观察矩阵,平移后绘制出背景四边形,绘制完同样恢复模型观察矩阵。
最后我们修改一下键盘控制函数,不多解释了,就是改了一下m_Object的最大值,具体代码如下:
void MyGLWidget::keyPressEvent(QKeyEvent *event)
{
switch (event->key())
{
case Qt::Key_F1: //F1为全屏和普通屏的切换键
fullscreen = !fullscreen;
if (fullscreen)
{
showFullScreen();
}
else
{
showNormal();
}
break;
case Qt::Key_Escape: //ESC为退出键
close();
break;
case Qt::Key_L: //L为开启关闭光源的切换键
m_Light = !m_Light;
if (m_Light)
{
glEnable(GL_LIGHTING); //开启光源
}
else
{
glDisable(GL_LIGHTING); //关闭光源
}
break;
case Qt::Key_Space: //空格为物体的切换键
m_Object++;
if (m_Object == 4)
{
m_Object = 0;
}
break;
case Qt::Key_PageUp: //PageUp按下使木箱移向屏幕内部
m_Deep -= 0.1f;
break;
case Qt::Key_PageDown: //PageDown按下使木箱移向观察者
m_Deep += 0.1f;
break;
case Qt::Key_Up: //Up按下减少m_xSpeed
m_xSpeed -= 0.1f;
break;
case Qt::Key_Down: //Down按下增加m_xSpeed
m_xSpeed += 0.1f;
break;
case Qt::Key_Right: //Right按下减少m_ySpeed
m_ySpeed -= 0.1f;
break;
case Qt::Key_Left: //Left按下增加m_ySpeed
m_ySpeed += 0.1f;
break;
}
}
现在就可以运行程序查看效果了!
全部教程中需要的资源文件点此下载
一点内容的补充:这里我准备解释一下上面glDrawCube()函数中法线范围修改的问题,大家先看下图(左侧为修改前,右面为修改后):
很明显左侧立方体中,对于环境的映射有块状现象,模糊了很多。这是由于glNormal()函数会根据参数对纹理坐标进行一定比例的放大缩小,1.0时保持原状,大于1.0时为放大,小于1.0时为缩小,所以我们把参数方位调整为[-0.5, 0.5]实际上缩小纹理图像(这里说的放大缩小是针对图像的,实际纹理坐标范围是相反的;可以想象,纹理坐标范围缩小,映射出来的图像就像被放大了一样)。
那为什么右侧的图里立方体上的纹理看起来还是比背景图大呢?大家不要忘了,我们在绘制背景图时还向屏幕里移入24.0单位呢,看起来小没什么问题吧。