【Qt OpenGL教程】26:剪裁平面,蒙板缓存和反射
来源:互联网 发布:宝黛爱情知乎 编辑:程序博客网 时间:2024/05/16 17:17
第26课:剪裁平面,蒙板缓存和反射 (参照NeHe)
这次教程中,我们将学会如何创建镜面显示效果,它利用到剪裁平面,蒙板缓存等OpenGL中一些高级的技巧。在这课里,我们将创建真正的反射,基于物理的,相信你一定很期待!
程序运行时效果如下:
下面进入教程:
我们这次将在第07课的基础上修改代码(有不少重复的地方),我会对新增部分一一解释。首先打开myglwidget.h文件,将类声明更改如下:
#ifndef MYGLWIDGET_H#define MYGLWIDGET_H#include <QWidget>#include <QGLWidget>class GLUquadric;class MyGLWidget : public QGLWidget{ Q_OBJECTpublic: 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; }}现在就可以运行程序查看效果了!
全部教程中需要的资源文件点此下载
0 0
- 【Qt OpenGL教程】26:剪裁平面,蒙板缓存和反射
- 【Qt OpenGL教程】24:扩展、剪裁和TGA图像文件的手动加载
- OpenGL中剪裁平面与模型视图变换的关系
- Part9 使用蒙板、剪裁空间(应用:实现反射效果)
- 蒙板缓存(结合nehe反射教程看)
- 蒙板缓存(结合nehe反射教程看)
- glClipPlane剪裁平面
- Qt OpenGL教程
- Qt OpenGL教程
- 【Qt OpenGL教程】07:光照和键盘控制
- openGL es2.0 创建颜色平面和文理平面
- OpenGL学习三十四:剪裁区域和TGA图像文件的加载
- openGL剪裁的使用
- OpenGL ES 剪裁
- OpenGL直线剪裁
- Qt OpenGL教程 (非常详细)
- 【Qt OpenGL教程】04:旋转
- 【Qt OpenGL教程】08:混合
- 问题描述 超时时间已到。超时时间已到,但是尚未从池中获取连接。出现这种情况可能是因为所有池连接均在使用,并且达到了最大池大小
- Energy Function
- dojo enhancedgrid的使用
- Android 自定义ListView
- 浅谈tableView内存优化行高问题
- 【Qt OpenGL教程】26:剪裁平面,蒙板缓存和反射
- java.lang.OutOfMemoryError: Java heap space
- 如何查看思科交换机的出厂时间
- arm汇编—str指令
- 设计模式:23 烤羊肉串引来的思考_命令模式
- 二叉树的C++实现
- java jaxb
- 使用FormData对象
- pushViewController and presentViewController are not working.