OpenGL案例-通过图元绘制了解矩阵栈

242 阅读6分钟

一、案例及相关函数说明

使用OpenGL绘制点、线、线段、线环、金字塔、六边形图形,并且使用键盘的空格键完成图形切换,使用上下左右键控制图形的上下左右旋转。效果如下:

二、预备知识

1.所需变量和类:

// 各种需要的类
GLShaderManager		shaderManager;
GLMatrixStack		modelViewMatrix;
GLMatrixStack		projectionMatrix;
GLFrame				cameraFrame;
GLFrame             objectFrame;
//投影矩阵
GLFrustum			viewFrustum;

//容器类(7种不同的图元对应7种容器对象)
GLBatch				pointBatch;
GLBatch				lineBatch;
GLBatch				lineStripBatch;
GLBatch				lineLoopBatch;
GLBatch				triangleBatch;
GLBatch             triangleStripBatch;
GLBatch             triangleFanBatch;

//几何变换的管道
GLGeometryTransform	transformPipeline;
//颜色
GLfloat vGreen[] = { 0.0f, 1.0f, 0.0f, 1.0f };
GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };


// 跟踪效果步骤,空格键记录
int nStep = 0;

重要名词:

  • 矩阵堆栈GLMatrixStack 变化管线使用矩阵堆栈,是用来存储我们矩阵的栈。 modelViewMatrix:模型视图矩阵栈,主要操作的就是这个矩阵,后面代码中会讲到。 projectionMatrix:透视投影矩阵栈

  • GLFrameGLFrame叫参考帧,其中存储了1个世界坐标点和2个世界坐标下的方向向量,也就是9个glFloat值,分别用来表示:当前位置点,向前方向向量,向上方向向量。

GLFrame可以表示世界坐标系中任意物体的位置与方向。无论是相机还是模型,都可以使用GLFrame来表示。对任意一个使用GLFrame来表示的物体而言,涉及到的坐标系有两个:永远不变的世界坐标系,针对于自身的物体坐标系(即绘图坐标系)。 简单理解GLFrame也是存储矩阵的,不过只能存一个。

函数:

本次案例除了之前会用到的几个函数传送门

  • int main(int argc,char *argv[]) main函数
  • void changeSize(int w ,int h):changeSize 函数
  • void RenderScene(void) RenderScene函数
  • void setupRC(void) setupRC函数
  • void SpecialKeys(int key, int x, int y) SpecialKey函数。

还新增了新的函数和部分变量: //根据空格次数。切换不同的“窗口名称”

  • void KeyPressFunc(unsigned char key, int x, int y): 点击空格时,调用的函数,用来处理按压空格后,调整显示图形的功能。

三、代码解析:

main函数:

int main(int argc, char* argv[])
{
    gltSetWorkingDirectory(argv[0]);
    glutInit(&argc, argv);
    //申请一个颜色缓存区、深度缓存区、双缓存区、模板缓存区
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
    //设置window 的尺寸
    glutInitWindowSize(800, 600);
    //创建window的名称
    glutCreateWindow("GL_POINTS");
    //注册回调函数(改变尺寸)
    glutReshapeFunc(ChangeSize);
    //点击空格时,调用的函数
    glutKeyboardFunc(KeyPressFunc);
    //特殊键位函数(上下左右)
    glutSpecialFunc(SpecialKeys);
    //显示函数
    glutDisplayFunc(RenderScene);
    
    //判断一下是否能初始化glew库,确保项目能正常使用OpenGL 框架
    GLenum err = glewInit();
    if (GLEW_OK != err) {
        fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
        return 1;
    }
    
    //绘制
    SetupRC();
    
    //runloop运行循环
    glutMainLoop();
    return 0;
}

main函数在之前就讲过,是作为程序启动的入口,在这里主要是做程序的准备工作。

changeSize函数:

// 窗口已更改大小,或刚刚创建。无论哪种情况,我们都需要
// 使用窗口维度设置视口和投影矩阵.
void ChangeSize(int w, int h)
{
    glViewport(0, 0, w, h);
    //创建投影矩阵,并将它载入投影矩阵堆栈中
    viewFrustum.SetPerspective(35.0f, float(w) / float(h), 1.0f, 500.0f);
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
    //调用顶部载入单元矩阵
    modelViewMatrix.LoadIdentity();
}

changeSize主要负责视口的变化处理,本案例中新增了创建投影矩阵并入栈到projectionMatrix透视矩阵栈和在modelViewMatrix模型视图矩栈中载入单元矩阵。

pressFun函数:

//根据空格次数。切换不同的“窗口名称”
void KeyPressFunc(unsigned char key, int x, int y)
{
    if(key == 32)
    {
        nStep++;
        
        if(nStep > 6)
            nStep = 0;
    }
    
    switch(nStep)
    {
        case 0:
            glutSetWindowTitle("GL_POINTS");
            break;
        case 1:
            glutSetWindowTitle("GL_LINES");
            break;
        case 2:
            glutSetWindowTitle("GL_LINE_STRIP");
            break;
        case 3:
            glutSetWindowTitle("GL_LINE_LOOP");
            break;
        case 4:
            glutSetWindowTitle("GL_TRIANGLES");
            break;
        case 5:
            glutSetWindowTitle("GL_TRIANGLE_STRIP");
            break;
        case 6:
            glutSetWindowTitle("GL_TRIANGLE_FAN");
            break;
    }
    
    glutPostRedisplay();
}

这个函数主要是记录当前的step,并根据当前step判断所需要显示的图形的名称,当显示完所有图形后,将step置为0,又从头开始显示。

specialKeys函数:

//特殊键位处理(上、下、左、右移动)
void SpecialKeys(int key, int x, int y)
{
    
    if(key == GLUT_KEY_UP)
        //围绕一个指定的X,Y,Z轴旋转。
        objectFrame.RotateWorld(m3dDegToRad(-5.0f), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_DOWN)
        objectFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_LEFT)
        objectFrame.RotateWorld(m3dDegToRad(-5.0f), 0.0f, 1.0f, 0.0f);
    
    if(key == GLUT_KEY_RIGHT)
        objectFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f);
    
    glutPostRedisplay();
}

specialKeys根据按压键位,判断当前显示图形该如何旋转。 这里要讲一下,我们常用的两种旋转方式:

  • 固定观察者,旋转模型。即上面代码中用到的:
objectFrame.RotateWorld()
  • 固定模型,旋转观察者。即:
cameraFrame.RotateWorld()

void RotateWorld(float fAngle, float x, float y, float z)函数: fAngle:表示旋转的角度 float x、y、z:相对X、Y、Z轴的位置 如果我们要围绕Y轴,让Y = 1,X = Z = 0即可; 反之围绕X轴:让X = 1,Y = Z = 0即可; 反之围绕Z轴:让Z = 1,X = Y = 0即可。

setupRC函数:

// 此函数在呈现上下文中进行任何必要的初始化。.
// 这是第一次做任何与opengl相关的任务。
void SetupRC()
{
    // 灰色的背景
    glClearColor(1.0f, 0.7f, 0.7f, 1.0f );
    shaderManager.InitializeStockShaders();
    glEnable(GL_DEPTH_TEST);
    //设置变换管线以使用两个矩阵堆栈
    transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
    //GLFrame中默认的朝向是z轴的负方向,即(0.0, 0.0, -1.0)
    //向前走-15.0,即(0.0, 0.0, -1.0 * -15.0) = (0.0, 0.0, 15.0)
    cameraFrame.MoveForward(-15.0f);
    
    //定义一些点,三角形形状。
   
    GLfloat vCoast[9] = {
        3,3,0,0,3,0,3,0,0
        
    };
    
    //用点的形式
    pointBatch.Begin(GL_POINTS, 3);
    pointBatch.CopyVertexData3f(vCoast);
    pointBatch.End();
    
    //通过线的形式
    lineBatch.Begin(GL_LINES, 3);
    lineBatch.CopyVertexData3f(vCoast);
    lineBatch.End();
    
    //通过线段的形式
    lineStripBatch.Begin(GL_LINE_STRIP, 3);
    lineStripBatch.CopyVertexData3f(vCoast);
    lineStripBatch.End();
    
    //通过线环的形式
    lineLoopBatch.Begin(GL_LINE_LOOP, 3);
    lineLoopBatch.CopyVertexData3f(vCoast);
    lineLoopBatch.End();
    
//    通过三角形创建金字塔
    GLfloat vPyramid[12][3] = {
        -2.0f, 0.0f, -2.0f,
        2.0f, 0.0f, -2.0f,
        0.0f, 4.0f, 0.0f,

        2.0f, 0.0f, -2.0f,
        2.0f, 0.0f, 2.0f,
        0.0f, 4.0f, 0.0f,

        2.0f, 0.0f, 2.0f,
        -2.0f, 0.0f, 2.0f,
        0.0f, 4.0f, 0.0f,

        -2.0f, 0.0f, 2.0f,
        -2.0f, 0.0f, -2.0f,
        0.0f, 4.0f, 0.0f
        
    };
    
    //GL_TRIANGLES 每3个顶点定义一个新的三角形
    triangleBatch.Begin(GL_TRIANGLES, 12);
    triangleBatch.CopyVertexData3f(vPyramid);
    triangleBatch.End();
    
    
    // 三角形扇形--六边形
    GLfloat vPoints[100][3];    
    int nVerts = 0;
    //半径
    GLfloat r = 3.0f;
    //原点(x,y,z) = (0,0,0);
    vPoints[nVerts][0] = 0.0f;
    vPoints[nVerts][1] = 0.0f;
    vPoints[nVerts][2] = 0.0f;
    
    
    //M3D_2PI 就是2Pi 的意思,就一个圆的意思。 绘制圆形
    for(GLfloat angle = 0; angle < M3D_2PI; angle += M3D_2PI / 6.0f) {
        
        //数组下标自增(每自增1次就表示一个顶点)
        nVerts++;
        /*
         弧长=半径*角度,这里的角度是弧度制,不是平时的角度制
         既然知道了cos值,那么角度=arccos,求一个反三角函数就行了
         */
        //x点坐标 cos(angle) * 半径
        vPoints[nVerts][0] = float(cos(angle)) * r;
        //y点坐标 sin(angle) * 半径
        vPoints[nVerts][1] = float(sin(angle)) * r;
        //z点的坐标
        vPoints[nVerts][2] = -0.5f;
    }
    
    // 结束扇形 前面一共绘制7个顶点(包括圆心)
    //添加闭合的终点
    //课程添加演示:屏蔽177-180行代码,并把绘制节点改为7.则三角形扇形是无法闭合的。
    nVerts++;
    vPoints[nVerts][0] = r;
    vPoints[nVerts][1] = 0;
    vPoints[nVerts][2] = 0.0f;
    
    // 加载!
    //GL_TRIANGLE_FAN 以一个圆心为中心呈扇形排列,共用相邻顶点的一组三角形
    triangleFanBatch.Begin(GL_TRIANGLE_FAN, 8);
    triangleFanBatch.CopyVertexData3f(vPoints);
    triangleFanBatch.End();
    
    //三角形条带,一个小环或圆柱段
    //顶点下标
    int iCounter = 0;
    //半径
    GLfloat radius = 3.0f;
    //从0度~360度,以0.3弧度为步长
    for(GLfloat angle = 0.0f; angle <= (2.0f*M3D_PI); angle += 0.3f)
    {
        //或许圆形的顶点的X,Y
        GLfloat x = radius * sin(angle);
        GLfloat y = radius * cos(angle);
        
        //绘制2个三角形(他们的x,y顶点一样,只是z点不一样)
        vPoints[iCounter][0] = x;
        vPoints[iCounter][1] = y;
        vPoints[iCounter][2] = -0.5;
        iCounter++;
        
        vPoints[iCounter][0] = x;
        vPoints[iCounter][1] = y;
        vPoints[iCounter][2] = 0.5;
        iCounter++;
    }
    
    // 关闭循环
    printf("三角形带的顶点数:%d\n",iCounter);
    //结束循环,在循环位置生成2个三角形
    vPoints[iCounter][0] = vPoints[0][0];
    vPoints[iCounter][1] = vPoints[0][1];
    vPoints[iCounter][2] = -0.5;
    iCounter++;
    
    vPoints[iCounter][0] = vPoints[1][0];
    vPoints[iCounter][1] = vPoints[1][1];
    vPoints[iCounter][2] = 0.5;
    iCounter++;
    
    // GL_TRIANGLE_STRIP 共用一个条带(strip)上的顶点的一组三角形
    triangleStripBatch.Begin(GL_TRIANGLE_STRIP, iCounter);
    triangleStripBatch.CopyVertexData3f(vPoints);
    triangleStripBatch.End();
}

这里面主要是负责计算我们图形显示所需的数据。 GLBatch相关函数解释:

//开始填充数据
void GLBatch::Begin(GLenum primitive,GLuint nVerts,GLuint nTextureUnits = 0);
      参数1:表示使用的图元
      参数2:顶点数
      参数3:纹理坐标(可选)
//负责顶点坐标,拷贝顶点坐标
void GLBatch::CopyVertexData3f(GLFloat *vNorms);
     
//填充结束,表示已经完成数据复制工作
 void GLBatch::End(void);

renderSence函数:

// 召唤场景
void RenderScene(void)
{
    // Clear the window with current clearing color
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
   
    //压栈
    modelViewMatrix.PushMatrix();
    M3DMatrix44f mCamera;
    cameraFrame.GetCameraMatrix(mCamera);
    
    //矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后简存储在堆栈的顶部
    modelViewMatrix.MultMatrix(mCamera);
    
    M3DMatrix44f mObjectFrame;
    //只要使用 GetMatrix 函数就可以获取矩阵堆栈顶部的值,这个函数可以进行2次重载。用来使用GLShaderManager 的使用。或者是获取顶部矩阵的顶点副本数据
    objectFrame.GetMatrix(mObjectFrame);
    
    //矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后简存储在堆栈的顶部
    modelViewMatrix.MultMatrix(mObjectFrame);
    
    /* GLShaderManager 中的Uniform 值——平面着色器
     参数1:平面着色器
     参数2:运行为几何图形变换指定一个 4 * 4变换矩阵
     --transformPipeline.GetModelViewProjectionMatrix() 获取的
     GetMatrix函数就可以获得矩阵堆栈顶部的值
     参数3:颜色值(黑色)
     */
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
    
    switch(nStep) {
        case 0:
            //设置点的大小
            glPointSize(6.0f);
            pointBatch.Draw();
            glPointSize(1.0f);
            break;
        case 1:
            //设置线的宽度
            glLineWidth(2.0f);
            lineBatch.Draw();
            glLineWidth(1.0f);
            break;
        case 2:
            glLineWidth(2.0f);
            lineStripBatch.Draw();
            glLineWidth(1.0f);
            break;
        case 3:
            glLineWidth(2.0f);
            lineLoopBatch.Draw();
            glLineWidth(1.0f);
            break;
        case 4:
            DrawWireFramedBatch(&triangleBatch);
            break;
        case 5:
            DrawWireFramedBatch(&triangleStripBatch);
            break;
        case 6:
            DrawWireFramedBatch(&triangleFanBatch);
            break;
    }
    
    //还原到以前的模型视图矩阵(单位矩阵)
    modelViewMatrix.PopMatrix();
    
    // 进行缓冲区交换
    glutSwapBuffers();
}

这里面主要是处理绘制图形逻辑。先PushMatrix ()压栈,然后获取当前的cameraFrame观察者和objectFrame模型视图矩阵并存储到矩阵栈,然后由transformPipeline获取MVP矩阵(GetModelViewProjectionMatrix()),然后交给着色器,最后进行PopMatrix()出栈。 其中:

  • 无论我们绘制那种图形,在绘制完成后都需要将我们的点、线宽进行重置。因为我们之前就讲到,OpenGL本身使用的时候就是一个巨大的状态机。如果当前绘制完成后不重置,就会影响后面的绘制。所以绘制完成后需要进行重置。
//设置线的宽度
 glLineWidth(2.0f);
///绘制
lineBatch.Draw();
//重置线宽
glLineWidth(1.0f);
  • shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);使用来绘制顶点,构建出我们所需要的显示图形框架;
  • void DrawWireFramedBatch(GLBatch* pBatch)函数是用来绘制填充框架,即我们看到的绿色部分。
void DrawWireFramedBatch(GLBatch* pBatch)
{
    /*------------画绿色部分----------------*/
    /* GLShaderManager 中的Uniform 值——平面着色器
     参数1:平面着色器
     参数2:运行为几何图形变换指定一个 4 * 4变换矩阵
          --transformPipeline 变换管线(指定了2个矩阵堆栈)
     参数3:颜色值
    */
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vGreen);
    pBatch->Draw();
    
    /*-----------边框部分-------------------*/
    /*
        glEnable(GLenum mode); 用于启用各种功能。功能由参数决定
        参数列表:http://blog.csdn.net/augusdi/article/details/23747081
        注意:glEnable() 不能写在glBegin() 和 glEnd()中间
        GL_POLYGON_OFFSET_LINE  根据函数glPolygonOffset的设置,启用线的深度偏移
        GL_LINE_SMOOTH          执行后,过虑线点的锯齿
        GL_BLEND                启用颜色混合。例如实现半透明效果
        GL_DEPTH_TEST           启用深度测试 根据坐标的远近自动隐藏被遮住的图形(材料
     
     
        glDisable(GLenum mode); 用于关闭指定的功能 功能由参数决定
     
     */
    
    //画黑色边框
    glPolygonOffset(-1.0f, -1.0f);// 偏移深度,在同一位置要绘制填充和边线,会产生z冲突,所以要偏移
    glEnable(GL_POLYGON_OFFSET_LINE);
    
    // 画反锯齿,让黑边好看些
    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    //绘制线框几何黑色版 三种模式,实心,边框,点,可以作用在正面,背面,或者两面
    //通过调用glPolygonMode将多边形正面或者背面设为线框模式,实现线框渲染
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    //设置线条宽度
    glLineWidth(2.5f);
    
    /* GLShaderManager 中的Uniform 值——平面着色器
     参数1:平面着色器
     参数2:运行为几何图形变换指定一个 4 * 4变换矩阵
         --transformPipeline.GetModelViewProjectionMatrix() 获取的
          GetMatrix函数就可以获得矩阵堆栈顶部的值
     参数3:颜色值(黑色)
     */
    
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
    pBatch->Draw();

    // 复原原本的设置
    //通过调用glPolygonMode将多边形正面或者背面设为全部填充模式
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    glDisable(GL_POLYGON_OFFSET_LINE);
    glLineWidth(1.0f);
    glDisable(GL_BLEND);
    glDisable(GL_LINE_SMOOTH);
    
    
}

这样就可以实现完成我们案例效果了。 通过这个案例,我们可以了解矩阵栈的操作过程:

  • 使用PushMatrix方法,会将栈顶信息复制一份,放入栈顶。
  • 使用MultMatrix方法做矩阵相乘时,将该矩阵与栈顶矩阵相乘,覆盖栈顶矩阵。
  • 使用PopMatrix做出栈操作时,移除栈顶矩阵对象。(根据栈的特点,只能pop栈顶)

当我们使用LoadMatrix加载矩阵到栈顶,若没有传参数矩阵,默认将单元矩阵压入栈顶

下一篇我们将尝试用OpenGL绘制一个甜甜圈,并解决其中会遇到的问题,想了解的同学可以关注下哦!

觉得不错记得点赞哦!听说看完点赞的人逢考必过,逢奖必中。ღ( ´・ᴗ・` )比心