OpenGL(八)- 模型视图矩阵
简介
模型视图矩阵式一个4×4矩阵,它表示一个变换后的坐标系,我们可以用来放置对象和确定对象的方向。
我们为图元提供的顶点将作为一个单列矩阵(也就是一个向量)的形式来使用,并乘以一个模型视图矩阵来获得一个相对于视觉坐标系的经过变换的新坐标。
如图所示,一个包含单个顶点数据的矩阵乘以模型视图矩阵后得到新的视觉坐标。
顶点数据实际上是4个元素,其中包含一个附加值W,它表示一个缩放因子。
默认情况下着个值设置为1.0,而我们很少去改动它。
矩阵构造
OpenGL并不是将一个4×4矩阵表示为一个浮点值的二维数组,而是将它表示为一个由16个浮点值组成的单个数组。
这种方式与许多数学库都不同,这些数学库都使用二维数组的方式。
例如,OpenGL会采用的是下面两个例子中的第一个。
GLfloat matrix[16];// Nice OpenGL friendly matrixGLfloat matrix[4][4];// Popular, but not as efficient for OpenGLOpenGL也可以使用第二种选择,但第一种是更加有效的表示方式。
这16个元素表示4×4矩阵,如图所示。
当这些数组元素一个接一个地遍历矩阵列中的列时,我们称这种方式为列优先排序。
在存储器中,这种4×4的二维数组形式(即上述代码的第二种选项)将以行优先顺序呈现。在数学中,这两种形式的矩阵互为转置矩阵。
这16个值表示了空间中的一个特定位置,以及相对于视觉坐标系(回忆一下我们以前讲过的那个固定的、没改变过的坐标系)的3个轴上的方向。
这4列中每一列都代表一个由4个元素组成的向量。为了保持本书简洁的风格,我们将注意力集中在前3列中的向量的前3个元素上。
第4列向量包含变换后的坐标系原点的x 、y 和z 值。
前3列的前3个元素只是方向向量,它们表示空间中x 轴、y 轴和z 轴上的方向(在这里用向量来表示一个方向)。
对于大多数应用来说,这3个向量相互之间总是成90°角,并且通常为单位长度(除非我们还应用了缩放或裁剪)。
这种情况的数学术语叫做标准正交(向量为单位长度)或者正交(向量不是单位长度)。
如果有一个包含一个不同坐标系的位置和方向的4×4矩阵,然后用一个表示原来坐标系的向量(表示为一个列矩阵或向量)乘以这个矩阵,得到的结果是一个转换到新坐标系下的新向量。
这就意味着,空间中任何位置和任何想要的方向都可以由一个4×4矩阵唯一确定,并且如果用一个对象的所有向量乘以这个矩阵,那么我们就将整个对象变换到了空间中的给定位置和方向!
单位矩阵
将一个向量乘以一个单位矩阵,就相当于用这个向量乘以1,不会发生任何改变。
如图4.13所示,单位矩阵中除了对角线上的一组元素之外,其他元素均为0。
使用单位矩阵绘制的对象不会发生变换,他们还在原点(最后一
列),并且x 轴、y 轴和z 轴与视觉坐标中一样。
我们可以在OpenGL中这样生成一个单位矩阵:
GLfloat m[]={1.0f,0.0f,0.0f,0.0f,// X Column0.0f,1.0f,0.0f,0.0f,// Y Column0.0f,0.0f,1.0f,0.0f,// Z Column0.0f,0.0f,0.0f,1.0f// Translation};或者使用math3d的M3DMatrix44f类型:
M3DMatrix44f m={1.0f,0.0f,0.0f,0.0f,// X Column0.0f,1.0f,0.0f,0.0f,// Y Column0.0f,0.0f,1.0f,0.0f,// Z Column0.0f,0.0f,0.0f,1.0f};// Translation在math3d库中还有一个快捷函数m3dLoadIdentity44,这个函数初始化
一个空单位矩阵。
voidm3dLoadIdentity44(M3DMatrix44f m);我们可以回忆一下,此前示例中使用的第一个存储(顶点)着色器就叫做单位着色器。
这个着色器完全不对顶点做任何改变,而是将这些顶点绘制在默认坐标系中,并且不在这些顶点上应用任何矩阵。
我们可以将这些顶点乘以单位矩阵,但是这是一种浪费和毫无意义的操作。
平移矩阵
一个平移矩阵仅仅是将我们的顶点沿着3个坐标轴中的一个或多个进行平移。
我们可以调用math3d库中的m3dTranslationMatrix44函数来使用变换矩阵。
voidm3dTranslationMatrix44(M3DMatrix44f m,floatx,floaty,floatz);旋转矩阵
为了将一个对象沿着3个坐标轴中的一个或者任意向量进行旋转,需要找到一个旋转矩阵,依旧可以使用math3d函数来完成。
m3dRotationMatrix44(M3DMatrix44f m,floatangle,floatx,floaty,floatz);在这里,我们围绕一个由x 、y 和z 变量指定的向量来进行旋转。
旋转的角度沿逆时针方向按照弧度计算,由变量angle指定。在最简单的情况下,旋转只是围绕坐标系的一个坐标轴(x 、y 或z )进行旋转。
我们还可以围绕任意一个由x 、y 和z 变量指定的向量来进行旋转。
要表示这个旋转轴,我们只要绘制一条从原点到由(x ,y ,z )表示的点的线段就可以了。举例来说,下面的代码创建一个旋转矩阵,可以使顶点沿着任意由(1,1,1)指定的轴旋转45°,如图所示。
M3DMatrix44f m;m3dRotationMatrix(m3dDegToRad(45.0),1.0f,1.0f,1.0f);缩放矩阵
最后一个矩阵是缩放矩阵。
缩放矩阵可以沿着3个坐标轴方向按照指定因子放大或缩小所有顶点,以改变对象的大小,使用math3d库创建一个缩放矩阵,方法与创建平移或旋转矩阵的方法类似。
M3DMatrix44f m;voidm3dScaleMatrix44(M3DMatrix44f m,floatxScale,floatyScale,floatzScale);缩放不一定是一致的,我们可以在不同的方向同时使用它来进行伸展和压缩。
例如,一个10×10×10的正方体可以在x方向和z方向放大到原来的两倍,如图所示。
综合变换
我们很少会只进行这些变换类型中的一种,实际上,我们总是想同时进行这些变换。
为了将对象移动到想要的位置,我们可能需要先将它平移到指定位置,然后再旋转到想要的方向。
由于4 x 4变换矩阵包含一个位置和一个方向,我们可能会想到,一个矩阵就可以完成这两种转换。
将两种变换加在一起很简单,只需将两个矩阵相乘。
结果得到的矩阵包含结合到一起的转换,都在一个矩阵中了。
“加”这个词在数学中当然是指加法,但实际上我们不会把这两个矩阵“加”到一起,它们是相乘的。
为了解决这个术语上的冲突,我们经常使用术语“连接”(concatenate)来表示两种变换以这种方式结合。
不过在矩阵乘法中有一个小陷阱需要注意,就是运算的顺序是有影响的。
例如,用一个旋转矩阵乘以一个平移矩阵,与用一个平移矩阵乘以一个旋转矩阵是不同的。
math3d库函数m3dMatrixMultiply44用来将两个矩阵相乘并返回运算结果。
voidm3dMatrixMultiply44(M3DMatrix44f product,constM3DMatrix44f a,constM3DMatrix44f b);示例
为了专注理解模型视图矩阵,以下示例中都固定投影矩阵,只是对模型视图矩阵进行变换。
矩形移动示例
代码如下:
#include"move_4.h"#include<GLTools.h>#include<GLShaderManager.h>#include<math3d.h>#ifndefFREEGLUT_STATIC#defineFREEGLUT_STATIC#endif#include<GL/glut.h>namespacemove_4{staticGLBatch squareBatch;staticGLShaderManager shaderManager;staticGLfloat blockSize=0.1f;staticGLfloat vVerts[]={-blockSize,-blockSize,0.0f,blockSize,-blockSize,0.0f,blockSize,blockSize,0.0f,-blockSize,blockSize,0.0f};staticGLfloat xPos=0.0f;staticGLfloat yPos=0.0f;voidSetupRC(){glClearColor(0.0f,0.0f,1.0f,1.0f);shaderManager.InitializeStockShaders();squareBatch.Begin(GL_QUADS,4);squareBatch.CopyVertexData3f(vVerts);squareBatch.End();}voidSpecialKeys(intkey,intx,inty){GLfloat stepSize=0.025f;if(key==GLUT_KEY_UP)yPos+=stepSize;if(key==GLUT_KEY_DOWN)yPos-=stepSize;if(key==GLUT_KEY_LEFT)xPos-=stepSize;if(key==GLUT_KEY_RIGHT)xPos+=stepSize;// Collision detectionif(xPos<(-1.0f+blockSize))xPos=-1.0f+blockSize;if(xPos>(1.0f-blockSize))xPos=1.0f-blockSize;if(yPos<(-1.0f+blockSize))yPos=-1.0f+blockSize;if(yPos>(1.0f-blockSize))yPos=1.0f-blockSize;glutPostRedisplay();}voidRenderScene(){glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);GLfloat vRed[]={1.0f,0.0f,0.0f,1.0f};M3DMatrix44f mFinalTransform,mTranslationMatrix,mRotationMatrix;// 创建平移矩阵m3dTranslationMatrix44(mTranslationMatrix,xPos,yPos,0.0f);// 创建旋转矩阵staticfloatyRot=0.0f;yRot+=0.5f;m3dRotationMatrix44(mRotationMatrix,m3dDegToRad(yRot),0.0f,0.0f,1.0f);// 矩阵乘法,旋转和平移矩阵相乘,得到组合后的变换矩阵m3dMatrixMultiply44(mFinalTransform,mTranslationMatrix,mRotationMatrix);// 提交绘制, mFinalTransform 作为 MVP 矩阵(模型-视图-投影矩阵)传递给 GPUshaderManager.UseStockShader(GLT_SHADER_FLAT,mFinalTransform,vRed);squareBatch.Draw();glutSwapBuffers();}voidChangeSize(intw,inth){glViewport(0,0,w,h);}intmove_test(intargc,char*argv[]){gltSetWorkingDirectory(argv[0]);glutInit(&argc,argv);glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);glutInitWindowSize(800,600);glutCreateWindow("Move Block with Arrow Keys");GLenum err=glewInit();if(GLEW_OK!=err){// fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));return1;}glutReshapeFunc(ChangeSize);glutDisplayFunc(RenderScene);glutSpecialFunc(SpecialKeys);SetupRC();glutMainLoop();return0;}}效果预览:
示例中我们让一个红色的方块随着按动方向键而在窗口中移动。
这里使用的是强制性的方法,即更新三角形扇的坐标,然后重建图元批次。
能够达到同样效果更好的方法是,一次性创建批次(通常是以原点为中心),然后在渲染这个批次时对顶点应用一个矩阵(实际上就是模型视图矩阵)。
在原来的程序中,我们使用了单位着色器,这个着色器对顶点不做任何变换,它只是传递这些顶点并在默认的笛卡尔坐标系中对它们进行渲染。
另一个存储着色器,即平面着色器,接受4×4变换矩阵作为它的参数之一。
GLShaderManager::UseStockShader(GLT_SHADER_FLAT,M3DMatrix44f m,GLfloat vColor[4]);这个着色器在对图元进行渲染之前用每个向量乘以矩阵m。
我们使用两个变量yPos和xPos来记录正方形的位置,现在可以方便地创建一个变换矩阵。
m3dTranslationMatrix44(mTranslationMatrix,xPos,yPos,0.0f);然后,这个变换矩阵就可以在绘制对象之前被发送到着色器。
shaderManager.UseStockShader(GLT_SHADER_FLAT,mTranslationMatrix,vRed);squareBatch.Draw();需要注意的是在RenderScene函数中,我们是如何分别创建平移矩mTranslationMatrix和旋转矩阵mRotationMatrix的,然后将它们相乘,以创建最终的变换矩阵。
m3dMatrixMultiply44(mFinalTransform,mTranslationMatrix,mRotationMatrix);更多几何示例
代码如下:
#include"objects.h"#include<stdio.h>#include<GLTools.h>#include<GLMatrixStack.h>#include<GLFrame.h>#include<GLFrustum.h>#include<GLBatch.h>#include<GLGeometryTransform.h>#include<math.h>#ifndefFREEGLUT_STATIC#defineFREEGLUT_STATIC#endif#include<GL/glut.h>namespaceobjects{staticGLShaderManager shaderManager;GLMatrixStack modelViewMatrix;GLMatrixStack projectionMatrix;GLFrame cameraFrame;GLFrame objectFrame;GLFrustum viewFrustum;GLTriangleBatch sphereBatch;GLTriangleBatch torusBatch;GLTriangleBatch cylinderBatch;GLTriangleBatch coneBatch;GLTriangleBatch diskBatch;GLGeometryTransform transformPipeline;M3DMatrix44f shadowMatrix;staticGLfloat vGreen[]={0.0f,1.0f,0.0f,1.0f};staticGLfloat vBlack[]={0.0f,0.0f,0.0f,1.0f};staticintnStep=0;voidSetupRC(){glClearColor(0.7f,0.7f,0.7f,1.0f);shaderManager.InitializeStockShaders();glEnable(GL_DEPTH_TEST);transformPipeline.SetMatrixStacks(modelViewMatrix,projectionMatrix);cameraFrame.MoveForward(-15.0f);gltMakeSphere(sphereBatch,3.0f,10,20);gltMakeTorus(torusBatch,3.0f,0.75f,15,15);gltMakeCylinder(cylinderBatch,2.0f,2.0f,3.0f,13,2);gltMakeCylinder(coneBatch,2.0f,0.0f,3.0f,13,2);gltMakeDisk(diskBatch,1.5f,3.0f,13,3);}voidDrawWireFramedBatch(GLTriangleBatch*pBatch){shaderManager.UseStockShader(GLT_SHADER_FLAT,transformPipeline.GetModelViewProjectionMatrix(),vGreen);pBatch->Draw();glPolygonOffset(-1.0f,-1.0f);glEnable(GL_LINE_SMOOTH);glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);glEnable(GL_POLYGON_OFFSET_FILL);glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);glLineWidth(2.5f);shaderManager.UseStockShader(GLT_SHADER_FLAT,transformPipeline.GetModelViewProjectionMatrix(),vBlack);pBatch->Draw();glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);glDisable(GL_POLYGON_OFFSET_LINE);glLineWidth(1.0f);glDisable(GL_BLEND);glDisable(GL_LINE_SMOOTH);}voidRenderScene(){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;objectFrame.GetMatrix(mObjectFrame);modelViewMatrix.MultMatrix(mObjectFrame);shaderManager.UseStockShader(GLT_SHADER_FLAT,transformPipeline.GetModelViewProjectionMatrix(),vBlack);switch(nStep){case0:DrawWireFramedBatch(&sphereBatch);break;case1:DrawWireFramedBatch(&torusBatch);break;case2:DrawWireFramedBatch(&cylinderBatch);break;case3:DrawWireFramedBatch(&coneBatch);break;case4:DrawWireFramedBatch(&diskBatch);break;}modelViewMatrix.PopMatrix();glutSwapBuffers();}voidSpecialKeys(intkey,intx,inty){// fprintf(stderr, "[DEBUG] Special Key Pressed: %d\n", key);if(key==GLUT_KEY_UP)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();}voidKeyPressFunc(unsignedcharkey,intx,inty){// fprintf(stderr, "Key press: %c\n", key);if(key==32){nStep++;if(nStep>4)nStep=0;}switch(nStep){case0:glutSetWindowTitle("Sphere");break;case1:glutSetWindowTitle("Torus");break;case2:glutSetWindowTitle("Cylinder");break;case3:glutSetWindowTitle("Cone");break;case4:glutSetWindowTitle("Disk");break;}glutPostRedisplay();}voidChangeSize(intw,inth){glViewport(0,0,w,h);viewFrustum.SetPerspective(35.0f,float(w)/float(h),1.0f,500.0f);projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());modelViewMatrix.LoadIdentity();}intobjects_test(intargc,char*argv[]){gltSetWorkingDirectory(argv[0]);glutInit(&argc,argv);glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);glutInitWindowSize(800,600);glutCreateWindow("Sphere");glutReshapeFunc(ChangeSize);glutKeyboardFunc(KeyPressFunc);glutSpecialFunc(SpecialKeys);glutDisplayFunc(RenderScene);GLenum err=glewInit();if(GLEW_OK!=err){// fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));return1;}SetupRC();glutMainLoop();return0;}}效果预览:
