ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

OpenGL(八)- 模型视图矩阵

2026/8/7 9:30:43 拓冰建站 浏览量
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);示例为了专注理解模型视图矩阵以下示例中都固定投影矩阵只是对模型视图矩阵进行变换。矩形移动示例代码如下#includemove_4.h#includeGLTools.h#includeGLShaderManager.h#includemath3d.h#ifndefFREEGLUT_STATIC#defineFREEGLUT_STATIC#endif#includeGL/glut.hnamespacemove_4{staticGLBatch squareBatch;staticGLShaderManager shaderManager;staticGLfloat blockSize0.1f;staticGLfloat vVerts[]{-blockSize,-blockSize,0.0f,blockSize,-blockSize,0.0f,blockSize,blockSize,0.0f,-blockSize,blockSize,0.0f};staticGLfloat xPos0.0f;staticGLfloat yPos0.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 stepSize0.025f;if(keyGLUT_KEY_UP)yPosstepSize;if(keyGLUT_KEY_DOWN)yPos-stepSize;if(keyGLUT_KEY_LEFT)xPos-stepSize;if(keyGLUT_KEY_RIGHT)xPosstepSize;// Collision detectionif(xPos(-1.0fblockSize))xPos-1.0fblockSize;if(xPos(1.0f-blockSize))xPos1.0f-blockSize;if(yPos(-1.0fblockSize))yPos-1.0fblockSize;if(yPos(1.0f-blockSize))yPos1.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);// 创建旋转矩阵staticfloatyRot0.0f;yRot0.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 errglewInit();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);更多几何示例代码如下#includeobjects.h#includestdio.h#includeGLTools.h#includeGLMatrixStack.h#includeGLFrame.h#includeGLFrustum.h#includeGLBatch.h#includeGLGeometryTransform.h#includemath.h#ifndefFREEGLUT_STATIC#defineFREEGLUT_STATIC#endif#includeGL/glut.hnamespaceobjects{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};staticintnStep0;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(keyGLUT_KEY_UP)objectFrame.RotateWorld(m3dDegToRad(-5.0f),1.0f,0.0f,0.0f);if(keyGLUT_KEY_DOWN)objectFrame.RotateWorld(m3dDegToRad(5.0f),1.0f,0.0f,0.0f);if(keyGLUT_KEY_LEFT)objectFrame.RotateWorld(m3dDegToRad(-5.0f),0.0f,1.0f,0.0f);if(keyGLUT_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(key32){nStep;if(nStep4)nStep0;}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 errglewInit();if(GLEW_OK!err){// fprintf(stderr, GLEW Error: %s\n, glewGetErrorString(err));return1;}SetupRC();glutMainLoop();return0;}}效果预览