第四章 OpenGL基础变换 向量和矩阵
在第三章,我们讨论了如何绘制3D点、线和三角形。为了将一系列图形转换到连续的场景,我们必须将它们相对于其他图形和观察者进行排列。在本章,我们开始学习在坐标系中移动图形。
本章内容:
- 什么是向量,以及为什么要了解它
- 什么是矩阵,以及为什么要更认真地了解它
- 我们如何使用矩阵和向量来移动几何图形
- OpenGL对于模型视图和投影矩阵的约定
- 什么是照相机,以及如何应用它转换
- 如何将一个点光源位置转换到视点坐标系
一、3D图形数学
GLTools库中有一个组建叫做Math3d,其中包含了大量好用的与OpenGL一致的3D数学例程和数据结构。
向量:
一个顶点是XYZ坐标空间上的一个位置,同时也是一个向量。
向量指出方向;同时也代表数量,一个向量的数量就是这个向量的长度。
Math3d库有两种数据类型:M3DVector3f表示一个三维向量(X,Y,Z);
M3DVector4f表示一个四维向量(X, Y, Z, W)。
点乘:两个单位向量之间的点乘运算将得到一个标量。它表示两个向量之间的夹角。要进行这种运算,两个向量必须为单位长度。而返回的结果将在-1和+1之间,实际是两个向量之间夹角的余弦值。
//返回余弦值
float m3dDotProduct3(const M3DVector3f u, M3DVector3f v);
//返回弧度值
float m3dGetAngleBetweenVetors3(const M3DVector3f u, M3DVector3f v);
叉乘:两个向量之间叉乘所得的结果是另外一个向量,这个新的向量与原来两个向量定义的平面垂直。叉乘的两个向量都不必为单位向量。两个向量位置交换叉乘值不同,即叉乘不满足交换律。
//返回结果向量
float m3dCrossProduct3(M3DVector3f result, const M3DVector3f u, M3DVector3f v);
矩阵:
如果空间中有一点,有X,Y,Z坐标定义,将它围绕任意点沿任意方向旋转一定角度后,我们需要知道这个点现在的位置,就要用到矩阵。数学上,矩阵为一组排列在统一的行和列中的数字,用程序设计语言来说就是一个二维数组。
3D程序设计中用到的几乎全是两种维度的矩阵,即3×3和4×4;在math3d库中有两种维度的矩阵类型:
typedef float M3DMatrix33f[9];
typedef float M3DMatrix44f[16];
二、理解变换
视觉坐标:
视觉坐标是相对于观察者的视角而言的,无论可能进行何种变换,我们都可以将它们视为“绝对的”屏幕坐标。
视图变换:
视图变换是应用到场景中的第一种变换。对于视觉坐标系而言,视图变换移动了当前的工作坐标系。所有后续变换随后都会基于新调整的坐标系进行。然后,在实际开始考虑如何进行这些变换时,就会更容易地看到这些变换是如何实现的了。
模型变换:
模型变换用于操纵模型和其中的特定对象。这些变换将对象移动到需要的位置,然后再对它们进行旋转和缩放。
模型视图的二元性:
实际上,视图和模型变换按照它们内部效果和对场景的最终外观来说是一样的。将这两者分开纯粹是为了程序员的方便。将对象象后移动和将参考系坐标向前移动在视觉上没有区别,效果是相同的。视图变换和模型变换一样,都应用在整个场景中,在场景中的对象常常在进行视图变换后单独进行模型变换。术语“模型视图”是指这两种变换在变换管线中进行组合,成为一个单独的矩阵,即模型视图矩阵。
投影变换:
投影变换将在模型视图变换之后应用到顶点上,这种投影实际上定义了视景体并创建了裁剪平面。投影变换指定一个完成的场景(所有模型变换都已完成)是如何投影到屏幕上的最终图形。
在正投影中,所有多边形都是精确地按照指定的相对大小来在屏幕上绘制的。线和多边形使用平行线来直接映射到2D屏幕上,无论物体位置远近,都按照相同大小来进行绘制。典型情况下,这个投影用于渲染如屏幕菜单等二维图像。
透视投影所显示的场景与现实生活中更接近,透视投影的特点就是透视缩短,这种特性使得远处的物体看起来比近处同样大小的物体更小一些。
视口变换:
当上述变换完成时,就得到了一个场景的二维投影,它将被映射到屏幕上某处的窗口上,这种到物理窗口坐标的映射是我们最后要做的变换,成为视口变换。
矩阵变换:
/*
mView: 平移
mModel: 旋转
mModelView: 模型视图
mModelViewProjection: 模型视图投影MVP
*/
M3DMatrix44f mView, mModel, mModelView, mModelViewProjection;
//mModel旋转矩阵,绕y轴旋转yRot度
m3dRotationMatrix44(mModel, m3dDegToRad(yRot), 0.0f, 1.0f, 0.0f);
//mView平移矩阵,沿z轴移动-2.5
m3dTranslationMatrix44(mView, 0.0f, 0.0f, -2.5f);
//mModelview = mView * mModel
m3dMatrixMultiply44(mModelview, mView, mModel);
//mModelViewProjection = ProjectionMatrix * mView * mModel
m3dMatrixMultiply44(mModelViewProjection,
viewFrustum.GetProjectionMatrix(),
mModelview);
更多对象:
GLBatch类,这个类的目的是为了解决容纳一个顶点列表并将它们作为一个特定类型的图元批次来进行渲染。而GLTriangleBatch,这个类是专门作为三角形的容器的,每个顶点都可以有一个表面法线,已进行光照计算和纹理坐标。
变换管线:
使用矩阵堆栈:
我们在矩阵储存时可能用到以下实例:
GLMatrixStack modelViewMatrix;
GLMatrixStack projectionMatrix;
GLFrame cameraFrame;
GLFrame objectFrame;
//其中,cameraFrame用于存储观察者矩阵,objectFrame用于存储模型矩阵。projectionMatrix只用于存储投影矩阵,我们操作最多的是modelViewMatrix。
//模型矩阵绕世界坐标系y轴旋转-5.0度
objectFrame.RotateWorld(m3dDegToRad(-5.0f), 0.0f, 1.0f, 0.0f);
//观察者矩阵向后退15.0,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);渲染过程中矩阵栈操作,获取MVP矩阵的计算结果
//压栈
modelViewMatrix.PushMatrix();
//获取观察者矩阵
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);
//栈顶矩阵乘以传入矩阵,相乘的结果简存储在栈顶
modelViewMatrix.MultMatrix(mCamera);
//获取模型矩阵
M3DMatrix44f mObjectFrame;
objectFrame.GetMatrix(mObjectFrame);
//由于先乘的观察者矩阵,现在再乘以模型矩阵
//栈顶 = M_view * M_model
modelViewMatrix.MultMatrix(mObjectFrame);
...
//由于初始化时传入了,modelViewMatrix和projectionMatrix的引用
//transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
//下面的代码会计算projectionMatrix * modelViewMatrix
//结合上面的代码,等价于M_projection * M_view * M_model
transformPipeline.GetModelViewProjectionMatrix()
...
//出栈
modelViewMatrix.PopMatrix();
其中,入栈,是为了保存当前的矩阵栈状态;出栈,是为了恢复入栈前的矩阵栈状态。这个操作类似于iOS的Core Graphic中context的保存与恢复。
//保存
void CGContextSaveGState(CGContextRef c);
//恢复
void CGContextRestoreGState(CGContextRef c);
优化矩阵栈操作:
不同的实现方式,之前我们用cameraFrame和objectFrame来记录相机和模型的变化,用到了两个对象。但我们可以固定一个对象,变化另一个对象。例如,之前的做法需要观察者向后退,同时物体旋转,把变化作用到了两个物体上,所以用到了两个矩阵分别记录两个物体的变化,最后再使用矩阵相乘把两个变化合并起来。
如果固定一个物体,那么根据相对运动,就是把之前两个物体的变化,相对地作用到另一个物体上,就可以少用一个矩阵了。
网友评论