OpenGL入门 (六) —— 矩阵基础变化实战

1,790 阅读7分钟

前言

本篇文章的目的主要是为了理解在OpenGL应该如何利用矩阵堆栈对3D图形进行基础变化操作,。由于之前的整个流程已经详细介绍步骤,这里着重讲其中的矩阵部分。(由于年底,最近太忙,能抽出的时间太少了,所以复习整理的比较慢~)

三种基础仿射变化在OpenGL中用矩阵堆栈类操作的代码实现:

//Rotate(旋转) 函数angle参数是传递的度数,⽽不是弧度,这个在后面我会用代码表现
void MatrixStack::Rotate(GLfloat angle,GLfloat x,GLfloat y,GLfloat z);
//平移
void MatrixStack::Translate(GLfloat x,GLfloat y,GLfloat z);
//缩放
void MatrixStack::Scale(GLfloat x,GLfloat y,GLfloat z);

1. 在ChangeSize方法中设置投影矩阵

这个方法在main函数中这样注册glutReshapeFunc(changeSize);

//窗口改变
void ChangeSize(int w, int h)
{
    //1.防止h变为0
    if(h == 0)
        h = 1;
    
    //2.设置视口窗口尺寸
    glViewport(0, 0, w, h);
    
    //3.setPerspective函数的参数是一个从顶点方向看去的视场角度(用角度值表示)
    // 设置透视模式(这里是透视投影,还有一个方法是设置正投影),初始化其透视矩阵
    viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f);
    
    //4.把透视矩阵加载到透视矩阵堆栈中
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
     //modelViewMatrix 矩阵堆栈 加载单元矩阵,这一步可以省略,因为堆栈底部默认就是一个单元矩阵
    modelViewMatrix.LoadIdentity();
    
    //5.初始化渲染管线
    transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix);
}

这里解释一下,projectionMatrix就是矩阵堆栈GLMatrixStack类型,LoadMatrix就是压栈,相当于pushMatrix。viewFrustum是GLFrustum类型(即视景体)

还有几个基本概念比较重要,需要理解一下

  • 视图:指定观察者位置
  • 模型:在场景中移动物体
  • 模型视图:描述视图/模型变换的二元性
  • 投影:改变视景体⼤小和设置它的投影⽅式
  • 视口:伪变化,对窗⼝上最终输出进行缩放

2. 在SetupRC方法中,初始化渲染环境

这个方法是在main函数中调用setupRC();,主要用来设置顶点,图形样式等基本渲染环境,在这里用三角形批次类GLTriangleBatch画一个甜甜圈

   // 1、设置背景颜色
    glClearColor(0.7f, 0.7f, 0.7f, 1.0f );

    // 2、初始化固定着色器管理器
    shaderManager.InitializeStockShaders();

    // 3、为了让效果明显,将观察者坐标位置Z移动往屏幕里移动15个单位位置
    // 参数:表示离屏幕之间的距离。 负数,是往屏幕后面移动;正数,往屏幕前面移动
    //GLFrame类型,表示camera
    cameraFrame.MoveForward(-15.0f);
//4.绘制甜甜圈
  //创建一个甜甜圈
    //void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
    //参数1:GLTriangleBatch 容器帮助类
    //参数2:外边缘半径
    //参数3:内边缘半径
    //参数4、5:主半径和从半径的细分单元数量
   
    gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
    

3. 在SpecialKeys方法里控制物体的移动,从而改变视口

这个方法是在main函数中注册的,glutSpecialFunc(SpeacialKeys);

//键位设置,通过键位对其进行设置
//控制Object的移动,从而改变视口,这里是对物体坐标系进行旋转
void SpecialKeys(int key, int x, int y)
{
    if(key == GLUT_KEY_UP)
        objectFrame.RotateWorld(m3dDegToRad(-5.0), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_DOWN)
        objectFrame.RotateWorld(m3dDegToRad(5.0), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_LEFT)
        objectFrame.RotateWorld(m3dDegToRad(-5.0), 0.0f, 1.0f, 0.0f);
    
    if(key == GLUT_KEY_RIGHT)
        objectFrame.RotateWorld(m3dDegToRad(5.0), 0.0f, 1.0f, 0.0f);
    
    //重新刷新window
    glutPostRedisplay();
}

4. 在RenderScene方法中,对上面画的甜甜圈做基础变换

在上面的代码中,glutPostRedisplay()方法也会调用RenderScene方法。 main函数中注册了该方法。

//清除窗口和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    //开启正背面剔除
        glEnable(GL_CULL_FACE);
    //开启深度测试
        glEnable(GL_DEPTH_TEST);
    
  //压栈
    modelViewMatrix.PushMatrix();
    //摄像机矩阵
    M3DMatrix44f mCamera;
    cameraFrame.GetCameraMatrix(mCamera);
    
    //矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后存储在堆栈的顶部
    modelViewMatrix.MultMatrix(mCamera);
    
    M3DMatrix44f mObjectFrame;
    //只要使用 GetMatrix 函数就可以获取矩阵堆栈顶部的值,这个函数可以进行2次重载。用来使用GLShaderManager 的使用。或者是获取顶部矩阵的顶点副本数据
    objectFrame.GetMatrix(mObjectFrame);
    
    //矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后简存储在堆栈的顶部
    modelViewMatrix.MultMatrix(mObjectFrame);
    
    GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
    
    //使用默认光源着色器
    //通过光源、阴影效果跟提现立体效果
    //参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
    //参数2:模型视图矩阵
    //参数3:投影矩阵
    //参数4:基本颜色值
    shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
    
    //绘制
    torusBatch.Draw();

    //出栈
    modelViewMatix.PopMatrix();
    
    
    glutSwapBuffers();

完成以上所有代码,即可在按上下左右方向键时,对画出来的甜甜圈进行旋转变化。

关键点分析

在上面的代码中,有几处需要重点理解的

1. transformPipeline

它是变换管道,类型是 GLGeometryTransform,专门用来管理投影和模型矩阵的。其实就是把两个矩阵堆栈都存到它这个管道里,方便我们用的时候拿出来。如果不使用这个管道,在RenderScene里这段代码

shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);

修改为从各自的堆栈里获取矩阵也能实现相同效果

shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, modelViewMatix.GetMatrix(), projectionMatrix.GetMatrix(), vRed);

在transformPipeline的源码里可以看到其实是一样的:

inline const M3DMatrix44f& GetModelViewMatrix(void) { return _mModelView->GetMatrix(); }
inline const M3DMatrix44f& GetProjectionMatrix(void) { return _mProjection->GetMatrix(); }

2. 关于modelViewMatix,projectionMatrix矩阵堆栈的压栈出栈原理

可以看下图理解(此处引用于:www.jianshu.com/p/ce3b51b8f…

(1)压栈 PushMatrix(); modelViewMatrix.PushMatrix(); 这句代码的意思是压栈,如果 PushMatix() 括号里是空的,就代表是把栈顶的矩阵复制一份,再压栈到它的顶部。如果不是空的,比如括号里是单元矩阵,那么就代表压入一个单元矩阵到栈顶了。

(2)矩阵相乘 MultMatrix(mObjectFrame) // 将modelViewMatrix 的堆栈中的矩阵 与 mOjbectFrame 矩阵相乘,存储到modelViewMatrix矩阵堆栈中modelViewMatrix.MultMatrix(mObjectFrame); 这句代码的意思是把 模型视图矩阵堆栈 的 栈顶 的矩阵copy出一份来和新矩阵进行矩阵相乘,然后再将相乘的结果赋值给栈顶的矩阵。

(3)出栈PopMatrix(); modelViewMatrix.PopMatrix(); 把栈顶的矩阵出栈,恢复为原始的矩阵堆栈,这样就不会影响后续的操作了。

3. 关于MultMatrix

我上面的代码

M3DMatrix44f mObjectFrame;
    //只要使用 GetMatrix 函数就可以获取矩阵堆栈顶部的值,这个函数可以进行2次重载。用来使用GLShaderManager 的使用。或者是获取顶部矩阵的顶点副本数据
    objectFrame.GetMatrix(mObjectFrame);
    
    //矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后简存储在堆栈的顶部
    modelViewMatrix.MultMatrix(mObjectFrame);

其实这处还有一种写法,

//把objectFrame矩阵压入模型矩阵中
modelViewMatix.MultMatrix(objectFrame);

其实两种写法是一样的,只因为我们这里传的参数类型不同,可以看源码MultMatrix函数理解

inline void MultMatrix(const M3DMatrix44f mMatrix) {
			M3DMatrix44f mTemp;
			m3dCopyMatrix44(mTemp, pStack[stackPointer]);
			m3dMatrixMultiply44(pStack[stackPointer], mTemp, mMatrix);
			}
            
        inline void MultMatrix(GLFrame& frame) {
            M3DMatrix44f m;
            frame.GetMatrix(m);
            MultMatrix(m);
            }

注意:modelViewMatix.MultMatrix(cameraFrame);是不可以的!!!仔细看MultMatrix(GLFrame& frame)函数里面写的是GetMatrix而不是GetCameraMatrix

4. 关于objectFrame和cameraFrame

前者是表示物体位置,后者是表示摄像机位置,看上面的代码,获取矩阵方式是不同的

cameraFrame.GetCameraMatrix(mCamera);
objectFrame.GetMatrix(mObjectFrame);

请认真看下图进行理解

总结

  1. 上面代码中没有完全提现前言中提到的所有三个仿射变化方法,可以自行用modelViewMatix在RenderScene函数中调用试试效果,便于理解。

  2. 如果能完全理解上述几个关键点,对OpenGL矩阵的基础变化差不多就吃透了,建议自己写一遍demo,看一万遍不如亲手写一遍代码。

点击查看本文完整demo

OpenGL入门 (一) —— OpenGL专业名词解析

OpenGL入门 (二) —— OpenGL Mac环境搭建

OpenGL入门 (三) —— 快速画一个正方形

OpenGL入门 (四) —— 渲染流程解析

OpenGL入门 (五) —— 图元绘制实战

OpenGL入门 (六) —— 矩阵基础变化实战

OpenGL入门 (七) —— 隐藏面消除详解

OpenGL入门 (八) —— 纹理坐标解析