【Qt OpenGL教程】26:剪裁平面,蒙板缓存和反射

第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;
    }
}
现在就可以运行程序查看效果了!


全部教程中需要的资源文件点此下载



猜你喜欢

转载自blog.csdn.net/cly116/article/details/47612073
今日推荐