【Qt OpenGL教程】27:阴影

第27课:阴影 (参照NeHe)

这次教程中,我们将介绍阴影的绘制,这将是一个高级的主题,请确信你已经熟练地了基本OpenGL,并熟悉蒙板缓存(我们第26课的内容)。当然,我们会一一解释,这次教程中的重点和难度,我希望你能喜欢它!

这一课中我们将介绍的阴影(影子),其效果好得有些让人不可思议,它可以变形,混合在其它的物体上,是不是很棒呢!当然,进入这一课之前,你必须对OpenGL比较了解,至少得懂得蒙板缓存了吧。还有,这一课中我们会用到部分高数和线代知识,我只会告诉大家是与哪方面的内容,详细的需要大家自己去查阅资料,当然你之前已经掌握就更好了。


程序运行时效果如下:



下面进入教程:


我们这次将在第01课的基础上修改代码,当然新增代码有不少你肯定已经懂的了(不懂请看前面教程吧),我不会再解释了。首先我们新创建一个类叫glObject,并打开globject.h文件,将类声明更改如下:

#ifndef GLOBJECT_H
#define GLOBJECT_H

#include <QWidget>
#include <QGLWidget>

struct sPoint                                       //3D顶点结构体
{
    GLfloat x, y, z;
};

struct sPlaneEq                                     //平面结构体(平面方程为ax+by+cz+d=0)
{
    GLfloat a, b, c, d;
};

struct sPlane                                       //三角形面结构体
{
    unsigned int p[3];                              //三角形面的三个顶点的编号
    sPoint normals[3];                              //三角形面的法线
    int neigh[3];                                   //与三角形三条边相邻的面的编号
    sPlaneEq planeEq;                               //三角形所在平面的平面方程
    bool visible;                                   //指明这个三角形是否面向光源
};

class glObject                                      //产生阴影的模型
{

public:
    glObject(QString filename);

    void draw();                                    //绘制模型
    void castShadow(GLfloat *lightPos);             //绘制阴影

private:
    void readData(QString filename);                //读取模型数据
    void calPlane(sPlane &plane);                   //计算平面方程的参数
    void setConnectivity();                         //设置相邻平面信息
    void doShadowPass(GLfloat *lightPos);           //绘制阴影边界的投影

private:
    int nPoints;                                    //模型的顶点数
    QVector<sPoint> vPoints;                        //储存顶点的向量
    int nPlanes;                                    //模型的三角形面数
    QVector<sPlane> vPlanes;                        //储存三角形面的向量
};

#endif // GLOBJECT_H
可以看到,我们在声明glObject之前,先定义了sPoint、sPlaneEq、sPlane三个结构体,依次代表3D顶点、平面方程、三角形面,结构体包含的内容大家自己看注释吧。然后我们声明glObject类,我们有nPoints、vPoints、nPlanes、vPlanes四个数据成员,依次指模型的顶点数、储存顶点的向量、三角形面数、储存三角形面的向量,说简单点,四个变量就代表模型的点和面的数据。

然后是3个public函数和4个private函数的声明,这些函数都是我们完成模型绘制和阴影绘制的重点。


我们打开object.cpp文件,加上声明#include <GL/glu.h>、#include <QFile>、#include <QTextStream>。我们先来看与模型数据读取以及初始化设置相关的readData()、setConnectivity()、calPlane()、glObject()(构造函数)等函数。四个函数下面我会分开解释,具体代码如下:

void glObject::readData(QString filename)           //读取模型数据
{
    QFile file(filename);
    file.open(QIODevice::ReadOnly | QIODevice::Text);//将要读入数据的文本打开
    QTextStream in(&file);                          //创建文本流

    in >> nPoints;                                  //读取模型的顶点数
    vPoints.push_back(sPoint());
    for (int i=0; i<nPoints; i++)                   //循环读取每个顶点数据
    {
        sPoint tPoint;
        in >> tPoint.x >> tPoint.y >> tPoint.z;
        vPoints.push_back(tPoint);
    }

    in >> nPlanes;                                  //读取模型的三角形面数
    for (int i=0; i<nPlanes; i++)                   //循环读取每个三角形面数据
    {
        sPlane tPlane;
        in >> tPlane.p[0] >> tPlane.p[1] >> tPlane.p[2]
           >> tPlane.normals[0].x >> tPlane.normals[0].y >> tPlane.normals[0].z
           >> tPlane.normals[1].x >> tPlane.normals[1].y >> tPlane.normals[1].z
           >> tPlane.normals[2].x >> tPlane.normals[2].y >> tPlane.normals[2].z;
        //初始化三角形面的邻面编号为-1(表示未设置或没有邻面)
        tPlane.neigh[0] = tPlane.neigh[1] = tPlane.neigh[2] = -1;
        vPlanes.push_back(tPlane);
    }

    file.close();
}
<pre name="code" class="cpp">void glObject::setConnectivity()                    //设置相邻平面信息
{
    for (int i=0; i<nPlanes-1; i++)                 //对于模型中的每一个面A
    {
        for (int j=i+1; j<nPlanes; j++)             //对于除了此面的其它面B
        {
            for (int ki=0; ki<3; ki++)              //对于A中的每一条边(当前顶点与下一顶点组成一条边)
            {
                if (vPlanes[i].neigh[ki] == -1)     //如果这条边的邻面没有被设置
                {
                    for (int kj=0; kj<3; kj++)      //对于B中的每一条边
                    {
                        int p1i = ki;
                        int p1j = kj;
                        int p2i = (ki+1)%3;
                        int p2j = (kj+1)%3;

                        p1i = vPlanes[i].p[p1i];    //A当前顶点编号
                        p1j = vPlanes[j].p[p1j];    //B当前顶点编号
                        p2i = vPlanes[i].p[p2i];    //A下一顶点编号
                        p2j = vPlanes[j].p[p2j];    //B下一顶点编号

                        int P1i = ((p1i+p2i) - abs(p1i-p2i)) / 2;   //A两个顶点(编号)较小者
                        int P1j = ((p1j+p2j) - abs(p1j-p2j)) / 2;   //B两个顶点较小者
                        int P2i = ((p1i+p2i) + abs(p1i-p2i)) / 2;   //A两个顶点较大者
                        int P2j = ((p1j+p2j) + abs(p1j-p2j)) / 2;   //B两个顶点较大者

                        if ((P1i == P1j) && (P2i == P2j))   //如果两顶点编号相同,说明相邻
                        {
                            vPlanes[i].neigh[ki] = j;       //相互设置为邻面
                            vPlanes[j].neigh[kj] = i;
                        }
                    }
                }
            }
        }
    }
}

 
 
void glObject::calPlane(sPlane &plane)              //计算平面方程的参数
{
    //获得三角形面的三个顶点
    const sPoint &v1 = vPoints[plane.p[0]];
    const sPoint &v2 = vPoints[plane.p[1]];
    const sPoint &v3 = vPoints[plane.p[2]];

    //由高数知识可求得公式
    plane.planeEq.a = v1.y*(v2.z-v3.z) + v2.y*(v3.z-v1.z) + v3.y*(v1.z-v2.z);
    plane.planeEq.b = v1.z*(v2.x-v3.x) + v2.z*(v3.x-v1.x) + v3.z*(v1.x-v2.x);
    plane.planeEq.c = v1.x*(v2.y-v3.y) + v2.x*(v3.y-v1.y) + v3.x*(v1.y-v2.y);
    plane.planeEq.d = -(v1.x*(v2.y*v3.z-v3.y*v2.z) + v2.x*(v3.y*v1.z-v1.y*v3.z)
                        + v3.x*(v1.y*v2.z-v2.y*v1.z));
}
glObject::glObject(QString filename)
{
    readData(filename);
    setConnectivity();
    for (int i=0; i<nPlanes; i++)
    {
        calPlane(vPlanes[i]);
    }
}
首先是readData()函数,这个函数用来读入文本中保存的模型数据。我建议大家先看一下资源文件Object2.txt(文章最后有下载连接),就好明白很多了,我们要读入数据的文件是按一定格式写入数据的。首先我们以只读方式打开文件,以此创建文本流in。我们先读入nPoints后,要注意我们先push_back了一个没有自己去初始化的sPoint,这是因为我们写入数据的顶点编号是从1开始的,不包括0,所以我们就先push_back一个用来“占位”。接着就是循环读入每一个sPoint的数据,并储存到vPoints中。然后我以相同的方式,读入nPlanes和每一个sPlane的数据,要注意的是,我们的文本里只保存了sPlane的p[]和normals[]数据,并且我们需要给neigh[]赋值为-1,表示未设置或者不存在邻面(一个三角形面至多有3个邻面)。最后就把每一个sPlane储存到vPlanes中,读完数据就关闭文件。

接着是setConnectivity()函数,这个函数用来设置模型的邻面数据(neigh[])。我们第一层循环循环模型中的每一个平面A,第二层循环循环除了A之外的其他平面,这样我们就让每两个平面作为一组进行检查。在循环中,我们再循环A平面的每一条边(共3条边),如果这条边的邻面还没有被设置,则循环B平面的每一条边,到此我们让每两个不同平面的共6条边中取属于不同平面的2条边进行检查。在最内层的循环里,我们在p1i、p2i、p1j、p2j中储存了进行检查的2条边的四个顶点的编号。接着利用公式(a-b)-|a-b|和(a-b)+|a-b|使得P1i储存P1i和P2i的较小者,P2i储存较大者,而P1j和P2j同理。最后,检查P1i和P1j、P2i和P2j是否同时相等,相等说明是同一条边,则设置互为邻面。

然后是calPlane()函数,这个函数用来设置模型的平面方程数据(planeEq)。我觉得这个函数代码还是比较简单的,我们先取得传进来的平面的三个顶点,然后利用三个顶点的坐标计算就得到平面方程的参数值。至于计算公式是怎么来的,我只说涉及到向量的叉乘求平面法向量,点法式求平面方程两方面的知识。

最后是构造函数glObject()。函数比较简单,就是调用了上面的三个函数,完成对模型数据的初始化,不多解释了。


我们再来看与绘制相关的函数,draw()与模型本身的绘制相关,castShadow()和doShadowPass()与模型阴影(影子)的绘制相关,具体代码如下:

void glObject::draw()                               //绘制模型
{
    glBegin(GL_TRIANGLES);
    for (int i=0; i<nPlanes; i++)                   //循环绘制每一个三角形面
    {
        for (int j=0; j<3; j++)                     //循环绘制每一个顶点
        {
            glNormal3f(vPlanes[i].normals[j].x,
                       vPlanes[i].normals[j].y,
                       vPlanes[i].normals[j].z);
            glVertex3f(vPoints[vPlanes[i].p[j]].x,
                       vPoints[vPlanes[i].p[j]].y,
                       vPoints[vPlanes[i].p[j]].z);
        }
    }
    glEnd();
}

void glObject::doShadowPass(GLfloat *lightPos)      //绘制阴影边界的投影
{
    sPoint v1, v2;
    for (int i=0; i<nPlanes; i++)                   //循环每一个三角形面
    {
        if (vPlanes[i].visible)                     //如果面对灯光(背对灯光不会产生阴影)
        {
            for (int j=0; j<3; j++)                 //对于被灯光照射的面的每一个邻面
            {
                int k = vPlanes[i].neigh[j];
                //如果邻面不存在或不被灯光照射,那么这条边是阴影边界
                if ((k == -1) || (!vPlanes[k].visible))
                {
                    //获得这条边的两个顶点
                    int p1 = vPlanes[i].p[j];
                    int jj = (j+1)%3;
                    int p2 = vPlanes[i].p[jj];

                    //计算边的顶点到灯光的方向,并放大100倍
                    v1.x = (vPoints[p1].x - lightPos[0])*100;
                    v1.y = (vPoints[p1].y - lightPos[1])*100;
                    v1.z = (vPoints[p1].z - lightPos[2])*100;
                    v2.x = (vPoints[p2].x - lightPos[0])*100;
                    v2.y = (vPoints[p2].y - lightPos[1])*100;
                    v2.z = (vPoints[p2].z - lightPos[2])*100;

                    //绘制构成阴影体边界的面
                    glBegin(GL_TRIANGLE_STRIP);
                        glVertex3f(vPoints[p1].x, vPoints[p1].y,
                                   vPoints[p1].z);
                        glVertex3f(vPoints[p1].x + v1.x, vPoints[p1].y + v1.y,
                                   vPoints[p1].z + v1.z);
                        glVertex3f(vPoints[p2].x, vPoints[p2].y,
                                   vPoints[p2].z);
                        glVertex3f(vPoints[p2].x + v2.x, vPoints[p2].y + v2.y,
                                   vPoints[p2].z + v2.z);
                    glEnd();
                }
            }
        }
    }
}
void glObject::castShadow(GLfloat *lightPos)        //绘制阴影
{
    for (int i=0; i<nPlanes; i++)                   //设置哪些面是面向灯光(通过向量点乘与0比较判断)
    {
        const sPlaneEq &planeEq = vPlanes[i].planeEq;
        GLfloat side = planeEq.a*lightPos[0] + planeEq.b*lightPos[1]
                     + planeEq.c*lightPos[2] + planeEq.d*lightPos[3];
        if (side > 0)
        {
            vPlanes[i].visible = true;
        }
        else
        {
            vPlanes[i].visible = false;
        }
    }

    glDisable(GL_LIGHTING);                         //关闭灯光
    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);    //关闭颜色缓存的写入
    glDepthFunc(GL_LEQUAL);                         //设置深度比较函数
    glDepthMask(GL_FALSE);                          //关闭深度缓存的写入
    glEnable(GL_STENCIL_TEST);                      //使用蒙板缓存
    glStencilFunc(GL_ALWAYS, 1, 0xFFFFFFFF);        //设置蒙板函数

    //如果是逆时针(即面向视点)的多边形,通过了蒙板和深度测试,则把蒙板的值增加1
    glFrontFace(GL_CCW);                            //设置逆时针绘制为正面
    glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
    doShadowPass(lightPos);

    //如果是顺时针(即背向视点)的多边形,通过了蒙板和深度测试,则把蒙板的值减少1
    glFrontFace(GL_CW);                             //设置顺时针绘制为背面
    glStencilOp(GL_KEEP, GL_KEEP, GL_DECR);
    doShadowPass(lightPos);

    glFrontFace(GL_CCW);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);//恢复颜色缓存的写入

    glColor4f(0.0f, 0.0f, 0.0f, 0.4f);              //阴影的颜色
    glEnable(GL_BLEND);                             //启用混合
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  //设置混合因子
    glStencilFunc(GL_NOTEQUAL, 0, 0xFFFFFFFF);
    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
    glPushMatrix();                                 //保存模型观察矩阵
    glLoadIdentity();
    glBegin(GL_TRIANGLE_STRIP);
        glVertex3f(-0.1f, 0.1f, -0.1f);
        glVertex3f(-0.1f, -0.1f, -0.1f);
        glVertex3f(0.1f, 0.1f, -0.1f);
        glVertex3f(0.1f, -0.1f, -0.1f);
    glEnd();
    glPopMatrix();                                  //恢复模型观察矩阵

    glDisable(GL_BLEND);                            //恢复OpenGL其他设置
    glDepthFunc(GL_LEQUAL);
    glDepthMask(GL_TRUE);
    glEnable(GL_LIGHTING);
    glDisable(GL_STENCIL_TEST);
    glShadeModel(GL_SMOOTH);
}
首先是draw()函数。函数挺简单的,首先循环每一个三角形面,循环中再循环每一个三角形面的三个顶点,对每一个顶点设置法线法线并绘制,这样就OK了。

然后我们先来看doShadowPass()函数,这个函数会在castShadow()中被调用,作用是由光源位置准确投影出每一条模型阴影边界。具体点说,就是投影出影子的边界所对应的边,具体为什么只需要投影边界的,这是与“阴影锥”相关的内容(我转载了一篇文章希望能帮助大家理解)。这个函数中,我们先循环每一个三角形面,如果这个面是面向灯光的(背对灯光的面不需要绘制阴影,因为它的影子会被其它面对灯光的面覆盖,我们不用作此无用功),我们再检查该三角形面的每一个邻面,如果发现不存在邻面或邻面是背对灯光的,这说明这个邻面所对应的边是阴影边界(大家想象一下吧)。如果是我们绘制从光源到这个边界的射线,并延伸它用来构成四边形,我们具体的做法是,取得该边的两个顶点,计算两个顶点分别到光源的距离,并把这个具体扩大100倍,然后在原来两顶点坐标的加上扩大后的值作为另外两个顶点的坐标,绘制投影四边形(解释得有点乱,大家自己看看代码吧,感觉代码不难理解的)。

最后是castShadow()函数,这个函数是完成模型阴影绘制的。函数一开始我们利用平面的法向量和光源位置的向量点乘,来确定哪些三角形面是面对光源的,哪些是背对光源的。下面设置必要的状态来渲染阴影,首先我们关闭了光源和绘制颜色,因为我们不打算计算光照,这样可以节约计算量。接着设置深度缓存,深度测试还是需要的,但我们不希望我们的阴影像实体一样具有深度,所以关闭深度缓存,最后我们启用蒙板缓存,并设置蒙板测试总是通过。

现在到了阴影实际渲染的地方了,我们使用了上面的doShadowPass()函数来绘制阴影的边界面。我们通过两个步骤来绘制阴影,首先使用前向面增加阴影体在蒙板缓存中的值,接着使用后向面减少阴影体在蒙板缓存中的值。前向面指的是我们看得到的投影面,后向面是我们看不到的投影面,一个“阴影锥”中,阴影是由所有前向面的投影面减去所有后向面的投影面得到的。如下图中(1)(2)(3)均称为投影面,(1)(2)为前向面,(3)为后向面,(1)+(2)-(3)就能得到阴影:


代码中,我们还看到一个新函数glFrontFace()函数,它用来指定逆时针和顺时针绘制时哪个算正面,并且我们在initializeGL()函数中会调用启用剔除反面的功能(下面我们在initializeGL()会看到具体代码)。还要说的是在我们读入数据的文本中,我们是让顶点以正对时为逆时针顺序绘制的,所以当一个三角形面是我们看得到的时,它是逆时针绘制的;看不到时,是顺时针绘制的(这个点我也理解了很久,大家琢磨琢磨吧)。因此,第一步时,我们先让逆时针绘制算正面,这样由于剔除功能,我们看不到的面(后向面)不会被绘制(因为它们是顺时针绘制的,是反面),然后成功绘制的地方会增加蒙板缓存。第二步时,我们再让顺时针绘制算反面,这样只绘制了我们看不到的面,绘制的地方会减少蒙板缓存。这样一增一减就只剩下阴影位置的蒙板缓存值不为0了(大家可以把第二步注释掉看看效果,我这里给上NeHe的配图)。


最后我们重新设置为逆时针为正面,打开颜色写入,然后把阴影绘制上了颜色。学了上节课的蒙板缓存你应该知道,由于我们前面没有打开颜色写入,但启用了蒙板测试,我们绘制的阴影只是改变了蒙板缓存数据,我们是看不到的。这时候,我们让蒙板缓存数据不为0的地方绘制上颜色,就能显示出阴影了(没弄懂的朋友请参考第26课)!到这里,我们完成了我们的glObject类。


下面打开myglwidget.h文件,将类声明更改如下:

#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H

#include <QWidget>
#include <QGLWidget>

typedef float GLmatrix16f[16];                      //把float[16]重新命名
typedef float GLvector4f[4];                        //把float[4]重新命名

class GLUquadric;
class glObject;
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 VMatMult(GLmatrix16f M, GLvector4f v);     //完成矩阵乘法v=M*v
    void drawRoom();                                //绘制房间

private:
    bool fullscreen;                                //是否全屏显示

    GLfloat m_xRot;                                 //x旋转角度
    GLfloat m_yRot;                                 //y旋转角度
    GLfloat m_xSpeed;                               //x旋转速度
    GLfloat m_ySpeed;                               //y旋转速度

    glObject *obj;                                  //指向模型的指针
    GLUquadric *m_Quadratic;                        //二次几何体
};

#endif // MYGLWIDGET_H
我们觉得增加的变量这些应该很容易了,大家应该都能理解。就说一下两个typedef语句,分别把长度为16的float数组命名为GLmatrix16f,把长度为4的float数组命名为GLvector4f。再注意一下两个类glObject、GLUquadric的声明,还有增加的函数声明大家自己看下注释留个印象,下面还会讲定义。


然后打开myglwidget.cpp文件,加上声明#include "globject.h"、#include <QTimer>,将构造函数和析构函数修改如下(不多解释):

MyGLWidget::MyGLWidget(QWidget *parent) :
    QGLWidget(parent)
{
    fullscreen = false;

    m_xRot = 0.0f;
    m_yRot = 0.0f;
    m_xSpeed = 0.0f;
    m_ySpeed = 0.0f;
    obj = new glObject("D:/QtOpenGL/QtImage/Object2.txt");

    QTimer *timer = new QTimer(this);                   //创建一个定时器
    //将定时器的计时信号与updateGL()绑定
    connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
    timer->start(10);                                   //以10ms为一个计时周期
}
MyGLWidget::~MyGLWidget()
{
    delete obj;
    gluDeleteQuadric(m_Quadratic);
}

下面是新增函数VMatMult()和drawRoom()的定义,很简单不解释(矩阵乘法知识不懂的百度下吧),具体代码如下:

void MyGLWidget::VMatMult(GLmatrix16f M, GLvector4f v)  //完成矩阵乘法v=M*v
{
    GLfloat res[4];
    res[0] = M[0]*v[0] + M[4]*v[1] + M[8]*v[2] + M[12]*v[3];
    res[1] = M[1]*v[0] + M[5]*v[1] + M[9]*v[2] + M[13]*v[3];
    res[2] = M[2]*v[0] + M[6]*v[1] + M[10]*v[2] + M[14]*v[3];
    res[3] = M[3]*v[0] + M[7]*v[1] + M[11]*v[2] + M[15]*v[3];
    v[0] = res[0];
    v[1] = res[1];
    v[2] = res[2];
    v[3] = res[3];
}
void MyGLWidget::drawRoom()                             //绘制房间
{
    glBegin(GL_QUADS);
        //地面
        glNormal3f(0.0f, 1.0f, 0.0f);
        glVertex3f(-10.0f, -10.0f, -20.0f);
        glVertex3f(-10.0f, -10.0f, 20.0f);
        glVertex3f(10.0f, -10.0f, 20.0f);
        glVertex3f(10.0f, -10.0f, -20.0f);
        //天花板
        glNormal3f(0.0f, -1.0f, 0.0f);
        glVertex3f(-10.0f, 10.0f, 20.0f);
        glVertex3f(-10.0f, 10.0f, -20.0f);
        glVertex3f(10.0f, 10.0f, -20.0f);
        glVertex3f(10.0f, 10.0f, 20.0f);
        //前面
        glNormal3f(0.0f, 0.0f, 1.0f);
        glVertex3f(-10.0f, 10.0f, -20.0f);
        glVertex3f(-10.0f, -10.0f, -20.0f);
        glVertex3f(10.0f, -10.0f, -20.0f);
        glVertex3f(10.0f, 10.0f, -20.0f);
        //后面
        glNormal3f(0.0f, 0.0f, -1.0f);
        glVertex3f(10.0f, 10.0f, 20.0f);
        glVertex3f(10.0f, -10.0f, 20.0f);
        glVertex3f(-10.0f, -10.0f, 20.0f);
        glVertex3f(-10.0f, 10.0f, 20.0f);
        //左面
        glNormal3f(1.0f, 0.0f, 0.0f);
        glVertex3f(-10.0f, 10.0f, 20.0f);
        glVertex3f(-10.0f, -10.0f, 20.0f);
        glVertex3f(-10.0f, -10.0f, -20.0f);
        glVertex3f(-10.0f, 10.0f, -20.0f);
        //右面
        glNormal3f(-1.0f, 0.0f, 0.0f);
        glVertex3f(10.0f, 10.0f, -20.0f);
        glVertex3f(10.0f, -10.0f, -20.0f);
        glVertex3f(10.0f, -10.0f, 20.0f);
        glVertex3f(10.0f, 10.0f, 20.0f);
    glEnd();
}

继续是initializeGL()函数了,重要的地方我会解释的,具体代码如下:

void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
{
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
    glShadeModel(GL_SMOOTH);                            //启用阴影平滑
    glClearDepth(1.0);                                  //设置深度缓存
    glClearStencil(0);
    glEnable(GL_DEPTH_TEST);                            //启用深度测试
    glDepthFunc(GL_LEQUAL);                             //所作深度测试的类型
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正

    //光源部分
    GLfloat LightAmbient[] = {0.2f, 0.2f, 0.2f, 1.0f};  //环境光参数
    GLfloat LightDiffuse[] = {0.6f, 0.6f, 0.6f, 1.0f};  //漫散光参数
    GLfloat LightPosition[] = {0.0f, 5.0f, -4.0f, 1.0f};//光源位置
    GLfloat LightSpc[] = {-0.2f, -0.2f, 0.2f, 1.0f};    //反射光参数
    glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);     //设置环境光
    glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);     //设置漫射光
    glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);   //设置光源位置
    glLightfv(GL_LIGHT1, GL_SPECULAR, LightSpc);        //设置反射光
    glEnable(GL_LIGHT1);                                //启动一号光源
    glEnable(GL_LIGHTING);                              //打开光源

    //设置受光照的物体材质颜色
    GLfloat MatAmb[] = {0.4f, 0.4f, 0.4f, 1.0f};        //材质的环境颜色
    GLfloat MatDif[] = {0.2f, 0.6f, 0.9f, 1.0f};        //材质的散射颜色
    GLfloat MatSpc[] = {0.0f, 0.0f, 0.0f, 1.0f};        //材质的镜面反射颜色
    GLfloat MatShn[] = {0.0f};                          //镜面反射指数
    glMaterialfv(GL_FRONT, GL_AMBIENT, MatAmb);         //设置环境颜色
    glMaterialfv(GL_FRONT, GL_DIFFUSE, MatDif);         //设置散射颜色
    glMaterialfv(GL_FRONT, GL_SPECULAR, MatSpc);        //设置反射颜色
    glMaterialfv(GL_FRONT, GL_SHININESS, MatShn);       //设置反射指数

    glCullFace(GL_BACK);                                //设置剔除面为背面
    glEnable(GL_CULL_FACE);                             //启用剔除

    m_Quadratic = gluNewQuadric();                      //二次几何体的初始化
    gluQuadricNormals(m_Quadratic, GL_SMOOTH);
    gluQuadricTexture(m_Quadratic, GL_FALSE);
}
最开始部分的代码中,我们增加了glClearStencil()函数,用来指明清除蒙板缓存时的值。然后我们添加光源部分的代码,除了参数和前面不同外,就是多增加了反射光(其实这个设不设置对画面影响不大)并打开了光源。然后是受光照的物理材质颜色的设置部分,这一部分代码的作用是指明当物体守到光照是会显示出什么样的颜色,或者说是光的颜色(只是可以这么理解,并不准确)。如果去掉这部分的代码,我们的物体会呈现灰色,而不是如上面图片看到的蓝色。

在这之后,我们又出现了新的函数glCullFace(),它告诉OpenGL剔除掉哪些平面,并启用了剔除平面glEnable(GL_CULL_FACE)。所谓的剔除就是在绘制的时候忽略这种平面的绘制,不去绘制它。还记得我们前面的castShadow()函数吗?我们在函数中调用了glFontFace()来告诉OpenGL逆时针绘制还是顺时针绘制为正面,我们就是通过这两个者的配合来完成只绘制前向面或只绘制后向面的。


最后我们进入paintGL()函数,我会顺便把键盘控制函数的代码给出来,但后者我就不解释了,具体代码如下:

void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
{
    GLfloat LightPos[] = {0.0f, 5.0f, -4.0f, 1.0f};     //光源位置
    GLfloat SpherePos[] = {-4.0f, -5.0f, -6.0f};        //球体位置
    GLfloat ObjPos[] = {-2.0f, -2.0f, -5.0f};           //模型位置

    GLmatrix16f Minv;
    GLvector4f lp;

    //清空缓存
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glLoadIdentity();                                   //重置模型观察矩阵
    glTranslatef(0.0f, 0.0f, -20.0f);                   //移入屏幕20.0单位
    glLightfv(GL_LIGHT1, GL_POSITION, LightPos);        //设置光源位置
    //平移并绘制球体
    glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]);
    gluSphere(m_Quadratic, 1.5f, 64, 64);

    //下面计算光源在模型坐标系中的位置
    glLoadIdentity();
    glRotatef(-m_yRot, 0.0f, 1.0f, 0.0f);               //以相反顺序相反方向旋转和平移
    glRotatef(-m_xRot, 1.0f, 0.0f, 0.0f);
    glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]);
    glGetFloatv(GL_MODELVIEW_MATRIX, Minv);             //获得从世界坐标系变化到模型坐标系的变换矩阵
    lp[0] = LightPos[0];                                //保存光源位置
    lp[1] = LightPos[1];
    lp[2] = LightPos[2];
    lp[3] = LightPos[3];
    VMatMult(Minv, lp);                                 //矩阵乘法计算模型坐标系中的光源位置

    glLoadIdentity();
    glTranslatef(0.0f, 0.0f, -20.0f);
    drawRoom();                                         //绘制房间
    glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]);      //平移旋转顺序与上面相反
    glRotatef(m_xRot, 1.0f, 0.0f, 0.0f);
    glRotatef(m_yRot, 0.0f, 1.0f, 0.0f);
    obj->draw();                                        //绘制模型
    obj->castShadow(lp);                                //根据光源位置绘制阴影

    glColor4f(0.7f, 0.4f, 0.0f, 1.0f);                  //设置为橘黄色
    glDisable(GL_LIGHTING);                             //关闭光源(否则其他颜色绘制不出来)
    glDepthMask(GL_FALSE);                              //禁用深度缓存的写入
    glTranslatef(lp[0], lp[1], lp[2]);                  //平移并绘制光源
    gluSphere(m_Quadratic, 0.2f, 64, 64);
    glEnable(GL_LIGHTING);                              //打开光源
    glDepthMask(GL_TRUE);                               //启用深度缓存的写入

    m_xRot += m_xSpeed;                                 //模型绕x轴旋转
    m_yRot += m_ySpeed;                                 //模型绕y轴旋转
}
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_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;
    }
}
一开始定义了光源位置,球体位置,模型位置等。接着清空缓存,重置矩阵,经过平移和设置光源位置后,我们先把球体绘制出来。接下来我们需要求光源在物体坐标系中的位置,根据线性代数的知识,只要用从物体坐标系变换到世界坐标系的矩阵×光源在世界坐标系中的坐标就可以得到。为了得到物体坐标系变换到世界坐标系的矩阵,我们重置矩阵后,以相反顺序和相反方向旋转和平移视点,然后调用glGetFloatv()函数就可以得到我们要的矩阵。下面我们拷贝一份光源的位置,并进行矩阵乘法,得到了光源在物体坐标系中的位置。

然后我们重置了矩阵,绘制了房间,房间会因为光源的打开而呈现蓝色。接着,我们平移和旋转视点,调用draw()函数绘制出模型,再调用castShadow()函数绘制出模型的阴影。还有我们要来绘制一个橘黄色的球代表光源的位置,要注意我们必须关闭光源,不然我们设置的橘黄色颜色是显示不出来的,会被光源的材质颜色覆盖掉,绘制完后,恢复深度写入和打开光源。最后我们设置物体的旋转。好吧,我们终于完成了!

现在就可以运行程序查看效果了!


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


猜你喜欢

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