第26课:剪裁平面,蒙板缓存和反射 (参照NeHe)
这次教程中,我们将学会如何创建镜面显示效果,它利用到剪裁平面,蒙板缓存等OpenGL中一些高级的技巧。在这课里,我们将创建真正的反射,基于物理的,相信你一定很期待!
程序运行时效果如下:
下面进入教程:
我们这次将在第07课的基础上修改代码(有不少重复的地方),我会对新增部分一一解释。首先打开myglwidget.h文件,将类声明更改如下:
#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H
#include <QWidget>
#include <QGLWidget>
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 drawObject(); //绘制球体
void drawFloor(); //绘制地面
private:
bool fullscreen; //是否全屏显示
QString m_FileName[3]; //图片的路径及文件名
GLuint m_Texture[3]; //储存一个纹理
GLfloat m_xRot; //x旋转角度
GLfloat m_yRot; //y旋转角度
GLfloat m_xSpeed; //x旋转速度
GLfloat m_ySpeed; //y旋转速度
GLfloat m_Deep; //深入屏幕的距离
GLfloat m_Height; //球离开地面的高度
GLUquadric *m_Quadratic; //二次几何体
};
#endif // MYGLWIDGET_H
首先我们看到m_FileName和m_Texture变成长度为3的数组,因为我们会使用三张图片来加载三个不同的纹理。接着我们增加了GLfloat变量m_Height来表示球体离开地面的高度,GLUquadric指针m_Quadratic来指向一个二次几何体对象(
当然我们应该在类前面增加GLUquadric的声明)。最后,我们增加了两个函数声明drawObject()和drawFloor(),依次用来绘制球体和地面。
接下来,我们需要打开myglwidget.cpp,在构造函数中初始化新增变量,并修改析构函数,很简单不多解释,具体代码如下:
MyGLWidget::MyGLWidget(QWidget *parent) :
QGLWidget(parent)
{
fullscreen = false;
m_FileName[0] = "D:/QtOpenGL/QtImage/Envwall.bmp"; //应根据实际存放图片的路径进行修改
m_FileName[1] = "D:/QtOpenGL/QtImage/Ball.bmp";
m_FileName[2] = "D:/QtOpenGL/QtImage/Envroll.bmp";
m_xRot = 0.0f;
m_yRot = 0.0f;
m_xSpeed = 0.0f;
m_ySpeed = 0.0f;
m_Deep = -6.0f;
m_Height = 2.0f;
QTimer *timer = new QTimer(this); //创建一个定时器
//将定时器的计时信号与updateGL()绑定
connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
timer->start(10); //以10ms为一个计时周期
}
MyGLWidget::~MyGLWidget()
{
gluDeleteQuadric(m_Quadratic);
}
下面,我们需要定义我们新增的drawObject()函数和drawFloor()函数了,具体代码如下:
void MyGLWidget::drawObject() //绘制球体
{
glColor3f(1.0f, 1.0f, 1.0f); //设置为白色
glBindTexture(GL_TEXTURE_2D, m_Texture[1]); //设置为球的纹理
gluSphere(m_Quadratic, 0.35f, 64, 64); //绘制球
glBindTexture(GL_TEXTURE_2D, m_Texture[2]); //设置为环境纹理
glColor4f(1.0f, 1.0f, 1.0f, 0.4f); //使用alpha为40%的白色
glEnable(GL_BLEND); //启用混合
glBlendFunc(GL_SRC_ALPHA, GL_ONE); //设置混合因子
glEnable(GL_TEXTURE_GEN_S); //启用自动生成纹理坐标
glEnable(GL_TEXTURE_GEN_T);
gluSphere(m_Quadratic, 0.35f, 64, 64); //绘制球体,并混合
glDisable(GL_TEXTURE_GEN_S); //让OpenGL恢复为默认的属性
glDisable(GL_TEXTURE_GEN_T);
glDisable(GL_BLEND);
}
void MyGLWidget::drawFloor() //绘制地面
{
glBindTexture(GL_TEXTURE_2D, m_Texture[0]); //选择地面纹理,地面由一个四边形组成
glBegin(GL_QUADS);
glNormal3f(0.0f, 1.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f); //左下
glVertex3f(-2.0f, 0.0f, 2.0f);
glTexCoord2f(0.0f, 0.0f); //左上
glVertex3f(-2.0f, 0.0f, -2.0f);
glTexCoord2f(1.0f, 0.0f); //右上
glVertex3f(2.0f, 0.0f, -2.0f);
glTexCoord2f(1.0f, 1.0f); //右下
glVertex3f(2.0f, 0.0f, 2.0f);
glEnd();
}
首先是drawObject()函数。一开始,我们设置颜色为白色,避免绘制纹理时附带了别的颜色。接着选择球体的纹理,将球绘制出来。在绘制完第一个球体后,我们选择环境纹理在相同的位置再绘制另一个球体,并把这两个球按alpha混合起来。其实第二遍绘制,是为了通过纹理贴图的方式产生一种球表面反射光的效果,使球表面出现一些亮斑。而这个时候需要使用自动生成纹理坐标,使得无论球体如何旋转,亮斑都是在视图中一样的位置(不自动生成纹理坐标会使亮斑都在球表面一样的位置,这两者是不一样的),通过这样我们产生了一种固定光源照射着球体的效果。函数最后,我们应该让OpenGL恢复为默认的属性,因此我们禁用自动生成纹理坐标和混合。
然后是drawFloor()函数。这个函数很简单,就是选择了地面纹理,然后把地面四边形绘制出来。
然后,我们来修改initializeGL()函数,具体代码如下:
void MyGLWidget::initializeGL() //此处开始对OpenGL进行所以设置
{
m_Texture[0] = bindTexture(QPixmap(m_FileName[0])); //载入位图并转换成纹理
m_Texture[1] = bindTexture(QPixmap(m_FileName[1]));
m_Texture[2] = bindTexture(QPixmap(m_FileName[2]));
glEnable(GL_TEXTURE_2D); //启用纹理映射
glClearColor(0.2f, 0.5f, 1.0f, 1.0f); //浅蓝色背景
glShadeModel(GL_SMOOTH); //启用阴影平滑
glClearDepth(1.0); //设置深度缓存
glEnable(GL_DEPTH_TEST); //启用深度测试
glDepthFunc(GL_LEQUAL); //所作深度测试的类型
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //告诉系统对透视进行修正
glClearStencil(0); //设置蒙板值
m_Quadratic = gluNewQuadric(); //创建二次几何体
gluQuadricNormals(m_Quadratic, GLU_SMOOTH); //使用平滑法线
gluQuadricTexture(m_Quadratic, GL_TRUE); //使用纹理
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);//设置球纹理映射,自动生成纹理坐标
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
//光源部分
GLfloat LightAmbient[] = {0.7f, 0.7f, 0.7f, 1.0f}; //环境光参数
GLfloat LightDiffuse[] = {1.0f, 1.0f, 1.0f, 1.0f}; //漫散光参数
GLfloat LightPosition[] = {4.0f, 4.0f, 6.0f, 1.0f}; //光源位置
glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); //设置环境光
glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); //设置漫射光
glLightfv(GL_LIGHT1, GL_POSITION, LightPosition); //设置光源位置
glEnable(GL_LIGHT1); //启动一号光源
glEnable(GL_LIGHTING); //开启光源
}
注意到,一开始我们修改了加载图片并转换为纹理的部分,不多解释。接着,我们把清除屏幕的颜色设置为浅蓝色(这个其实问题不大),后面我们调用了一个新函数glClearStencil()用来指明清除蒙板缓存时,清除后各位置蒙板缓存的值为多少,这里我们设置为0。然后在创建二次几何体后,我们设置了自动生成纹理坐标的模式为球面映射(glTexGeni),这个前面我们在第23课专门讲过了。最后是修改了光源数据,并直接打开了光源,因为我们并不打算利用键盘来控制光源的打开关闭(当然有兴趣的朋友可以自己试试改改代码,
注意关闭光源时,之前绘制球亮斑的部分也不应该执行了)。
还有,我们该进入重点的paintGL()函数,具体代码如下:
void MyGLWidget::paintGL() //从这里开始进行所以的绘制
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT //清除缓存
| GL_STENCIL_BUFFER_BIT);
double eqr[] = {0.0f, -1.0f, 0.0f, 0.0f}; //设置剪切平面
GLfloat LightPosition[] = {4.0f, 4.0f, 6.0f, 1.0f}; //光源位置
glLoadIdentity(); //重置模型观察矩阵
glTranslatef(0.0f, -0.6f, m_Deep); //平移和缩放地面
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);//设置颜色掩码,不能绘制任何颜色
glEnable(GL_STENCIL_TEST); //启用蒙板缓存
glStencilFunc(GL_ALWAYS, 1, 1); //设置蒙板测试总是通过,参考值和掩码值均设为1
//设置当蒙板测试不通过时,保留蒙板中的值不变。如果通过则使用参考值代替蒙板值
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glDisable(GL_DEPTH_TEST); //禁用深度测试
drawFloor(); //绘制地面
glEnable(GL_DEPTH_TEST); //启用深度测试
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); //设置颜色掩码,可以绘制任何颜色
//下面的设置指定当我们绘制时,不改变蒙板缓存区的值
glStencilFunc(GL_LEQUAL, 1, 1);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glEnable(GL_CLIP_PLANE0); //使用剪切平面
glClipPlane(GL_CLIP_PLANE0, eqr); //设置剪切平面为地面,并设置它的法线向下
glPushMatrix(); //保存当前的矩阵
glScalef(1.0f, -1.0f, 1.0f); //沿y轴反转
glLightfv(GL_LIGHT0, GL_POSITION, LightPosition);
glTranslatef(0.0f, m_Height, 0.0f);
glRotatef(m_xRot, 1.0f, 0.0f, 0.0f);
glRotatef(m_yRot, 0.0f, 1.0f, 0.0f);
drawObject(); //绘制反射的球
glPopMatrix(); //弹出保存的矩阵
glDisable(GL_CLIP_PLANE0); //禁用剪切平面
glDisable(GL_STENCIL_TEST); //禁用蒙板测试
glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);
glEnable(GL_BLEND); //启用混合
glDisable(GL_LIGHTING); //关闭光源
glColor4f(1.0f, 1.0f, 1.0f, 0.8f); //设置颜色为白色
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //设置混合因子
drawFloor(); //绘制地面
glEnable(GL_LIGHTING); //打开光源
glDisable(GL_BLEND); //禁用混合
glTranslatef(0.0f, m_Height, 0.0f); //移动和旋转
glRotatef(m_xRot, 1.0f, 0.0f, 0.0f);
glRotatef(m_yRot, 0.0f, 1.0f, 0.0f);
drawObject(); //绘制真正的球
m_xRot += m_xSpeed; //球绕x轴旋转
m_yRot += m_ySpeed; //球绕y轴旋转
}
一开始,我们的清屏函数要修该下参数,我们不仅要清除颜色缓存(GL_COLOR_BUFFER_BIT)和深度缓存(GL_DEPTH_BUFFER_BIT),还要加上蒙板缓存(GL_STENCIL_BUFFER_BIT)。接着定义我们的剪切平面,它用来剪切我们的图像,这个平面的方程为equ[] = {0, -1, 0, 0}(其实就是平面0*x + (-1)*y + 0*z + 0 = 0),并且如我们所见它的法线方向指向-y轴,这个告诉OpenGL只绘制y坐标小于0的像素。还有我们定义了光源位置,数据与前面的initializeGL()函数中数据是一致的。
下面我们把模型观察矩阵重置,然后放大缩小视图,并把向下平移0.6f单位,因为我们的眼睛在y=0平面,如果不平移的话,如果不平移的话,那么看上去平面就会变成一条线,为了看起来更真实,我们平移了它。然后我们设置了颜色掩码(glColorMask),在默认情况下所有颜色都可以吸入,即在函数glColorMask中,所有的参数都被设为GL_TRUE。现在我们不希望在屏幕上绘制如何东西,所以把参数设为GL_FALSE。
下面来设置蒙板缓存和蒙板测试(重点)。首先我们启用蒙板测试(GL_STENCIL_TEST),这样就可以修改蒙板缓存中的值了,下面我们来解释蒙板测试函数的含义:
当你使用glEnable(GL_STENCIL_TEST)启用蒙板测试之后,蒙板函数用于确定一个颜色片段是应该丢弃还是保留(被绘制)。蒙板缓存中的值与参考值进行比较,比较标准是func所指定的比较函数。在比较之前,参考值和蒙板缓存中的值都会与先进行AND操作。蒙板测试的结果还导致蒙板缓存区根据glStencilOp函数所指定的行为进行修改。func的参数如下:
接下来我们解释glStencilOp函数,它用来指定根据比较结果修改蒙板缓存区中的值的方式,有三个参数:第一个参数表示当蒙板测试失败时所执行的操作;第二个参数表示当蒙板测试通过,深度测试失败时所执行的操作;第三个参数表示当蒙板测试通过,深度测试通过时所执行的操作。具体操作包括以下几种:
当完成了以上操作后,我们绘制了一个地面,当然现在我们是什么也看不到的,它只是用来把覆盖地面的蒙板缓存区中的相应位置设为1。
我们现在已经在蒙板缓存区中建立了地面的蒙板了,这将是绘制影子(反射)的关键。下面我们启用深度测试和绘制颜色(glColorMask),并相应的设置蒙板测试和函数的值,这种设置可以使我们在屏幕上绘制而不改变蒙板缓存区的值。然后我们设置并启用了剪切平面(glClipPlane),使得只能在地面的下方绘制(也就是只能绘制出y坐标小于0的点)。然后我们保存了当前的模型观察矩阵,并沿y轴反转,glScalef之前我们讲过参数大于1.0时为放大,小于1.0时为缩小,而现在参数为负时,则会进行相应方向上的反转。由于上面已经启用了蒙板缓存,则我们只能在蒙板缓存中值为1的地方绘制,反射的实质就是在反射屏幕的对应位置再绘制一个物体,并把它放置在反射屏幕中。如此,我们就成功再地面存在的区域绘制了球体的投影,绘制完后,我们恢复模型观察矩阵,并禁用剪切平面和蒙板测试。
然后我们来绘制真正看得到的地面,并把地面颜色和反射的球的颜色混合,使其看起来像反射效果。最后就是在距离地面高m_Height的地方绘制一个真正的球体了。函数结束前,根据旋转速度增加球的旋转角度,让球自动旋转起来。
最后,我们来修改键盘控制函数,很简单不多解释,具体代码如下:
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_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;
case Qt::Key_Q: //Q按下使球体上移
m_Height += 0.1f;
break;
case Qt::Key_Z: //Z按下使球体下移
m_Height -= 0.1f;
break;
}
}
现在就可以运行程序查看效果了!