OpenGL入门第三课--矩阵变换与坐标系统

    

       在 OpenGL中,物体在被渲染到屏幕以前须要通过一系列的坐标变换,听起来有点吓人;不过呢若是有必定的线性代数的基础利用矩阵变换,其实也就没那么难了。即便没学过线性代数,只须要了解一些基本的矩阵运算也基本能够知足你们学习 OpenGL的要求了。下面咱们就来简单学习一下有关矩阵的知识。动画

    矩阵是一种很是有用的数学工具,虽然有点难度,可是一旦你理解了它们后,它们会变得很是有用。为了深刻了解矩阵变换,咱们首先要在讨论矩阵以前了解一下向量。ui

向量

       向量最基本的定义就是一个方向。或者更正式的说,向量有一个方向(Direction)和大小(Magnitude,也叫作强度或长度)。你能够把向量想像成一个藏宝图上的指示:“向左走10步,向北走3步,而后向右走5步”;“左”就是方向,“10步”就是向量的长度。因为向量表示的是方向,起始于何处并不会改变它的值。以下图向量v和向量w就是相等的. 

                           

        向量能够在任意维度上,通常用到的都是2维和3维,一个三维向量在公式中一般是这样表示的。入下图:

  


                                         

      因为向量是一个方向,因此有些时候会很难形象地将它们用位置(Position)表示出来。为了让其更为直观,咱们一般设定这个方向的原点为(0, 0, 0),而后指向一个方向,对应一个点,使其变为位置向量(Position Vector)(你也能够把起点设置为其余的点,而后说:这个向量从这个点起始指向另外一个点)。好比说位置向量(3, 5)在图像中的起点会是(0, 0),并会指向(3, 5)。

向量与标量的运算

       标量(Scalar)只是一个数字(或者说是仅有一个份量的向量)。当把一个向量加/减/乘/除一个标量,咱们能够简单的把向量的每一个份量分别进行该运算。对于加法来讲会像这样:

                                              

      其中的+能够是+,-,·或÷,其中·是乘号。注意-和÷运算时不能颠倒(标量-/÷向量),由于颠倒的运算是没有定义的。

向量取反

     对一个向量取反(Negate)会将其方向逆转。一个指向东北的向量取反后就指向西南方向了。咱们在一个向量的每一个份量前加负号就能够实现取反了(或者说用-1数乘该向量):

                                       

向量相减

     向量的加法能够被定义为是份量的相加,即将一个向量中的每个份量加上另外一个向量的对应份量;就像普通数字的加减同样,向量的减法等于加上第二个向量的相反向量:

    

向量的长度

     咱们使用勾股定理来获取向量的长度:

                                                         

      咱们也能够加上Z的平方 把这个公式扩展到三维空间。另外有一个特殊类型的向量叫作单位向量(Unit Vector)。单位向量有一个特别的性质——它的长度是1。咱们能够用任意向量的每一个份量除以向量的长度获得它的单位向量。

向量相乘

     向量相乘分为点乘和叉乘:

点乘:两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。可能听起来有点费解,咱们来看一下公式。

                                   

      它们之间的夹角记做θ,这有什么用,想象一下假如是两个单位向量点乘会是什么结果?如今点积只定义了两个向量的夹角。是否还记得90度的余弦值是0,0度的余弦值是1。这样使用点乘能够很容易测试两个向量是否正交(Orthogonal)或平行(正交意味着两个向量互为直角)。

    也能够经过点乘的结果计算两个非单位向量的夹角,点乘的结果除以两个向量的长度之积,获得的结果就是夹角的余弦值,即 cosθ。因此,咱们该如何计算点乘呢?点乘是经过将对应份量逐个相乘,而后再把所得积相加来计算的。

叉乘:叉乘只在3D空间中有定义,它须要两个不平行向量做为输入,生成一个正交于两个输入向量的第三个向量.若是输入的两个向量也是正交的,那么叉乘以后将会产生3个互相正交的向量。下面的图片展现了3D空间中叉乘的样子:

                                

      不一样于其余运算,若是你没有钻研过线性代数,可能会以为叉乘很反直觉,因此只记住公式就没问题(记不住也没问题)。下面你会看到两个正交向量A和B叉积:

                

矩阵

       矩阵就是一个矩形的数学表达式阵列,矩阵中每一项叫作矩阵的元素(Element)。下面是一个2×3矩阵的例子:

                                             

     矩阵能够经过(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫作2×3矩阵的缘由。

矩阵的加减法

     矩阵与标量之间的加减定义以下:


矩阵与标量的减法也类似:


    矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,因此整体的规则和与标量运算是差很少的,只不过在相同索引下的元素才能进行运算。这也就是说加法和减法只对同维度的矩阵才是有定义的。一个3×2矩阵和一个2×3矩阵(或一个3×3矩阵与4×4矩阵)是不能进行加减的。咱们看看两个2×2矩阵是怎样相加的:


一样的法则也适用于减法:


矩阵的数乘

     和矩阵与标量的加减同样,矩阵与标量之间的乘法也是矩阵的每个元素分别乘以该标量。下面的例子展现了乘法的过程:

 

矩阵相乘

      矩阵之间的乘法不见得有多复杂,但的确很难让人适应。矩阵乘法基本上意味着遵守规定好的法则进行相乘。固然,相乘还有一些限制:

  • 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘;
  • 矩阵相乘不遵照交换律(Commutative),也就是说A⋅B≠B⋅A;

咱们先看一个两个2×2矩阵相乘的例子:


    能够看到,矩阵相乘很是繁琐而容易出错,不够没关系,在咱们OpenGL中是不会让你们本身去计算这些的,都有相应API来完成,这里只是但愿你们了解一下矩阵相乘的原理。

矩阵与向量相乘

    如今咱们已经至关了解向量了。在OpenGL中能够用向量来表示位置,表示颜色,甚至是纹理坐标。与矩阵类比一下,它其实就是一个N×1矩阵,N表示向量份量的个数(也叫N维(N-dimensional)向量)。仔细想一下,其实向量和矩阵同样都是一个数字序列,但它只有1列。那么,这个新的定义对咱们有什么帮助呢?若是咱们有一个M×N矩阵,咱们能够用这个矩阵乘以咱们的N×1向量,由于这个矩阵的列数等于向量的行数,因此它们就能相乘。

可是为何咱们会关心矩阵可否乘以一个向量?好吧,正巧,不少有趣的2D/3D变换均可以放在一个矩阵中,用这个矩阵乘以咱们的向量将变换(Transform)这个向量。若是你仍然有些困惑,咱们来看一些例子,你很快就能明白了。

单位矩阵

      在OpenGL中,因为某些缘由咱们一般使用4×4的变换矩阵,而其中最重要的缘由就是大部分的向量都是4份量的。咱们能想到的最简单的变换矩阵就是单位矩阵(Identity Matrix)。单位矩阵是一个除了对左角线是1之外其余都是0的N×N矩阵。在下式中能够看到,这种变换矩阵使一个向量彻底不变:

   

     你可能会奇怪一个没变换的变换矩阵有什么用?单位矩阵一般是生成其余变换矩阵的起点,若是咱们深挖线性代数,这仍是一个对证实定理、解线性方程很是有用的矩阵。

缩放

    下面会构造一个变换矩阵来为咱们提供缩放功能。咱们从单位矩阵了解到,每一个对角线元素会分别与向量的对应元素相乘。若是咱们把1变为3会怎样?这样子的话,咱们就把向量的每一个元素乘以3了,这事实上就把向量缩放3倍。若是咱们把缩放变量表示为(S1,S2,S3)咱们能够为任意向量(x,y,z)定义一个缩放矩阵:

          

     注意,第四个缩放向量仍然是1,由于在3D空间中缩放w份量是无心义的。w份量另有其余用途。

位移

    位移(Translation)是在原始向量的基础上加上另外一个向量从而得到一个在不一样位置的新向量的过程,从而在位移向量基础上移动了原始向量。咱们已经讨论了向量加法,因此这应该不会太陌生。和缩放矩阵同样,在4×4矩阵上有几个特别的位置用来执行特定的操做,对于位移来讲它们是第四列最上面的3个值。若是咱们把位移向量表示为(Tx,Ty,Tz),咱们就能把位移矩阵定义为:

        

    有了位移矩阵咱们就能够在3个方向(x、y、z)上移动物体,它是咱们的变换工具箱中很是有用的一个变换矩阵。

旋转

    在3D空间中旋转须要定义一个角和一个旋转轴(Rotation Axis)。物体会沿着给定的旋转轴旋转特定角度。当2D向量在3D空间中旋转时,咱们把旋转轴设为z轴。

     使用三角学,给定一个角度,能够把一个向量变换为一个通过旋转的新向量。这一般是使用一系列正弦和余弦函数(通常简称sin和cos)各类巧妙的组合获得的。

       旋转矩阵在3D空间中每一个单位轴都有不一样定义,旋转角度用θ表示:

沿x轴旋转:


沿y轴旋转:


沿z轴旋转:


    利用旋转矩阵咱们能够把任意位置向量沿一个单位旋转轴进行旋转。也能够将多个矩阵复合,好比先沿着x轴旋转再沿着y轴旋转。

    是否是感受旋转看起来好复杂,这些旋转矩阵都是怎么计算出来的,其实这些都不须要太过在乎,只有理解旋转也是经过一个特定的矩阵相乘完成的就能够了。

矩阵的组合

    使用矩阵进行变换的真正力量在于,根据矩阵之间的乘法,咱们能够把多个变换组合到一个矩阵中。让咱们看看咱们是否能生成一个变换矩阵,让它组合多个变换。假设咱们有一个顶点(x, y, z),咱们但愿将其缩放2倍,而后位移(1, 2, 3)个单位。咱们须要一个位移和缩放矩阵来完成这些变换。结果的变换矩阵看起来像这样:


    注意,当矩阵相乘时咱们先写位移再写缩放变换的。矩阵乘法是不遵照交换律的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,因此你应该从右向左读这个乘法。建议您在组合矩阵时,先进行缩放操做,而后是旋转,最后才是位移,不然它们会(消极地)互相影响。好比,若是你先位移再缩放,位移的向量也会一样被缩放(译注:好比向某方向移动2米,2米也许会被缩放成1米)!

用最终的变换矩阵左乘咱们的向量会获得如下结果:

           

不错!向量先缩放2倍,而后位移了(1, 2, 3)个单位。

OpenGL中的坐标系

       OpenGL中顶点着色后,咱们的可见顶点都为标准化设备坐标(Normailzed Device Coordinate,NDC)。也就是每一个顶点的x,y,z都应该在-1到1直接,不然对咱们都是不可见的。

      一个顶点在被转化为片断以前须要依次经历一下几个重要的坐标系:

  1. 局部空间(Local Space 或者称为 物体空间 Object Space)
  2. 世界空间(World Space)
  3. 观察空间 (View Space 或者称为 视觉空间 Eye Space)
  4. 裁剪空间(Clip Space)
  5. 屏幕空间 (Screen Space)

   从一个坐标系变到另一个坐标系须要利用变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵.物体顶点的起始坐标再局部空间(Local Space),这里称它为局部坐标(Local Coordinate),它在以后会变成世界坐标(world Coordinate),观测坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Corrdinate)的形式结束.

       想要把3D图形最终渲染到2D设备屏幕上,除了使用模型变换和视变换将物体坐标转换到照相机坐标系外,还须要进行投影变换将坐标变为裁剪坐标系,而后通过透视除法变换到规范化设备坐标系(NDC),最后进行视口变换渲染到2D屏幕上,以下图:


     在上面的图中,OpenGL只定义了裁剪坐标系、规范化设备坐标系和屏幕坐标系,而局部坐标系(物体坐标系)、世界坐标系和照相机坐标系都是为了方便用户设计而自定义的坐标系。也就是说,模型变换、视变换、投影变换,这些变换能够根据开发者的需求自行定义,这些内容在顶点着色器中完成。另外的两个透视除法和视口变换,这两个步骤是OpenGL自动执行的,在顶点着色器处理后的阶段完成。

     上面的每个变换都建立了一个变换矩阵,模型矩阵、观察矩阵和投影矩阵。讲这些矩阵组合起来,一个顶点坐标将会根据如下过程被变换到裁剪坐标:


     注意矩阵运算的顺序是相反的(记住咱们须要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

    说了那么多理论,好像都不知道怎么用,下面咱们来写一个简单的案例,看看如何利用矩阵变换讲3d图像到渲染到2d屏幕上。

案例

    咱们首先来看一下案例效果再来讲说如何实现:

                                                            

    这这个案例中咱们先经过先经过平移矩阵与旋转矩阵叉乘获得模型视图矩阵,而后经过投影矩阵叉乘模型视图矩阵获得模型视图投影矩阵也就是咱们常说的mvp,而后经过平面着色器画出图形。具体代码以下:

main函数一些初始化操做和回调函数的注册:

int main(int argc, char* argv[]){   
 gltSetWorkingDirectory(argv[0]);   
 glutInit(&argc, argv);   
 glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_SINGLE);   
 glutInitWindowSize(800, 600);   
 glutCreateWindow("矩阵变换Demo");   
 glutReshapeFunc(ChangeSize);   
 glutDisplayFunc(RenderScene);   
 GLenum error = glewInit();    
 if(GLEW_OK != error) {        
   fprintf(stderr,"GLEW Error: %s\n",glewGetErrorString(error));      
   return 1;  
  }   
 setupRC();   
 glutMainLoop();   
 return 0;
}复制代码

这里有三个方法比较重要 第一个 setupRC,主要完成一些绘图前的准备工做:

void setupRC () {
    glClearColor(0.8, 0.8, 0.8, 1.0f); //设置清屏颜色  
    shaderManager.InitializeStockShaders();//初始化固定管线
    glEnable(GL_DEPTH_TEST);//开启深度测试   
    gltMakeSphere(torusBatch, 0.4, 10, 20);//造成一个球    
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//设置多边形填充模式
}复制代码

    changeSize这个函数在初次窗口显示 或者其余任什么时候候窗口改变的时候将会被调用。主要完成视口和投影矩阵的设置,具体以下:

void ChangeSize(int w, int h){
    if(h == 0) h = 1;
    glViewport(0, 0, w, h);   
     viewFrustum.SetPerspective(35, float(w)/float(h), 1, 1000); 
 }复制代码

RenderScene函数就是具体完成绘制的函数,具体代码以下:

void RenderScene(void){    
    //清除屏幕、深度缓存区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    //1.创建基于时间变化的动画
    static CStopWatch rotTimer;
    //当前时间 * 60s
    float yRot = rotTimer.GetElapsedSeconds() * 60.0f;
    //2.矩阵变量
    /*
     mTranslate: 平移
     mRotate: 旋转
     mModelview: 模型视图
     mModelViewProjection: 模型视图投影MVP
     */
    M3DMatrix44f mTranslate, mRotate, mModelview, mModelViewProjection;
    //建立一个4*4矩阵变量,将花托沿着Z轴负方向移动2.5个单位长度
    m3dTranslationMatrix44(mTranslate, 0.0f, 0.0f, -2.5f);
    //建立一个4*4矩阵变量,将花托在Y轴上渲染yRot度,yRot根据通过时间设置动画帧率
     m3dRotationMatrix44(mRotate, m3dDegToRad(yRot), 0.0f, 1.0f, 0.0f);
    //为mModerView 经过矩阵旋转矩阵、移动矩阵相乘,将结果添加到mModerView上
    m3dMatrixMultiply44(mModelview, mTranslate, mRotate);
    // 将投影矩阵乘以模型视图矩阵,将变化结果经过矩阵乘法应用到mModelViewProjection矩阵上
    //注意顺序: 投影 * 模型 != 模型 * 投影
     m3dMatrixMultiply44(mModelViewProjection, viewFrustum.GetProjectionMatrix(),mModelview);
    //绘图颜色
    GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };
    //经过平面着色器提交矩阵,和颜色。
    shaderManager.UseStockShader(GLT_SHADER_FLAT, mModelViewProjection, vBlack);
    //开始绘图
    torusBatch.Draw();
    // 交换缓冲区,并当即刷新
    glutSwapBuffers();
    glutPostRedisplay();
} 复制代码

   这里咱们先建立了平移和旋转矩阵,经过叉乘 :平移矩阵 X 旋转矩阵 = 模型视图矩阵。从右往左度实际是旋转后平移,为何要先旋转后平移在上文矩阵的组合中已经说过了。

而后经过投影矩阵叉乘模型视图矩阵获得模型视图投影矩阵(mvp),这样就经过固定管线的平面着色器须要的参数就有了,而后调用相关OpenGL API 就能顺利完成绘制。

相关文章
相关标签/搜索