在正式进入本章节内容之前,先来正视几个观念。
1. 3D 数学在OpenGL中充当什么角色?
对于学习OpenGL 有?个误区,就是大家认为如果不能精通那些3D图形数学知识,会 让我们?步难行,其实不然。就像我们不需要懂得任何关于汽车结构和内燃机?面的 知识也能每天开车。但是,我们最好能对汽?车有?够的了解,以便我们意识到什么时 候需要更换机油、定期加油、汽?常规保养工作。
同样要成为一名可靠和有能力的OpenGL程序员,至少需要理解这些基础知识,才知道 能作什么?以及哪些工具适合我们要做的?作。
对于初学者,经过一段时间的实践,就会渐渐理解矩阵和向量。并且培养出一种更为直观 的能力,能够在实践中充分利用所学的内容。
2. GLTools 库中有一个组件叫Math3d
其中包含了了?量好?的OpenGL一致的3D数学和数据类型。虽然我们不必亲?进行所有的矩阵和向量的操作,但我然知道它们是什么?以及如何运?它们.
(一)向量和矩阵
1.1 向量
向量的定义
向量=方向+标量
- 方向:点(1,0,0),在x方向为1,y和z的方向都为0
- 标量:可以理解为长度,同样是点(1,0,0),在x方向的长度为1,y和z的为0
- 单位向量:标量(长度)为
1
的向量 - 标准化:将一个向量的长度缩放为
1
-
向量长度(模):
向量的使用
math3D
库中有两种数据类型
-
M3DVector3f
:三维向量 (x, y, z) -
M3DVector4f
:四维向量 (x, y, z, w),w表示缩放因子,通常设为1,x,y,z的值通过除以w,来进行缩放。而除以1.0是不会改变x,y,z的值。
//三维向量/四维向量的声明
typedef float M3DVector3f[3];
typedef float M3DVector4f[4];
//声明一个三维向量 M3DVector3f:类型 vVector:变量名
M3DVector3f vVector;
//声明一个四维向量并初始化?个四维向量
M3DVector4f vVertex = {0,0,1,1};
//声明一个三分量顶点数组,例如?成一个三?形
M3DVector3f vVerts[] = {
-0.5f,0.0f,0.0f,
0.5f,0.0f,0.0f,
0.0f,0.5f,0.0f
};
向量的点乘
向量可以进行 加法,减法计算. 但是向量里有?个在开发中使用价值?常?的操作,叫做“点乘(dot product)”。点乘只能发?在2个向量之间进行
。
2个(三维向量)单元向量 之间进行点乘运算将得到?个标量(不是三维向量,是?个标量
)。它表示两个向量之间的夹角。
- 条件: 2个向量必须为单位向;
- 动作: 2个三维向量之间进?点乘
- 结果:返回一个[-1,1]范围的值,这个值其实就是 夹?的cos值(余弦值),
点乘结果是一个标量
- 用途:游戏中计算子弹的夹角
math3d 库中提供了关于点乘的API
//1.m3dDotProduct3 函数获得2个向量之间的点乘结果;
float m3dDotProduct3(const M3DVector3f u,const M3DVector3f v);
//2.m3dGetAngleBetweenVector3 即可获取2个向量之间夹?的弧度值;
float m3dGetAngleBetweenVector3(const M3DVector3f u,const
M3DVector3f v);
向量的叉乘
向量之间的叉乘(cross product) 也是在业务开发?非常有用的一个计算?式;;2个向量之间叉乘就可以得到另外一个向量,新的向量会与原来2个向量定义的平?垂直
。同时进?叉乘,不必为单位向量。
- 前提:两个普通向量
- 动作:向量与向量叉乘
- 结果:向量(垂直于原来2个向量定义的平面),
叉乘结果是一个向量
- 注意:
这两个向量的顺序不一样,得到的结果向量方向相反
- 用途:游戏求法线(垂直于平面的线)
math3d 库中提供了关于叉乘的API
//1.m3dCrossProduct3 函数获得2个向量之间的叉乘结果得到一个新的向量
void m3dCrossProduct3(M3DVector3f result,const M3DVector3f u ,const
M3DVector3f v);
1.2 矩阵
矩阵的定义
- 矩阵的定义
假设, 在空间有?个点.使? xyz 描述它的位置. 此时让其围绕任意位置旋转一定?角度
后. 我们需要知道这个点的新的位置. 此时需要通过矩阵进行行计算; 为什么?
因为新的位置的x 单纯与原来的x还和旋转的参数有关. 甚?至于y和z坐标有关
- 矩阵只有一行或者一列都是合理的。
- 只有一行或一列的数字可以成为向量,也可以称为矩阵。
声明向量,用的是
- M3DVector3f[];
- M3DVector4f[];
声明矩阵,用的是
- M3DMatrix33f[];
- M3DMatrix44f[];
//三维矩阵/四维矩阵的声明
typedef float M3DMatrix33f[9];
typedef float M3DMatrix44f[16];
在OpenGL中,推荐使用一维数组来定义矩阵,也可以使用二维数组
- 行矩阵和列矩阵
- 行矩阵:行优先矩阵排序,图中按行读取,
A0->A1->A2->A3->...
- 列矩阵:列优先矩阵排序,图中按列读取,
A0->A1->A2->A3->...
- OpenGL 中以列优先,在数学中,称列矩阵为
转置矩阵
如何识别列矩阵,如下图中的例子
- 矩阵的最后一行都是0
- 最后一个元素为1
- 这16个值,每一个都对应一个特定的值
- 每一列分别对应一个“轴”
- 矩阵的最后一行都是0
- 最后一个元素为1
- 单元矩阵
向量中有单位向量(长度为一的向量),矩阵中也有单元矩阵
定义:
对角线上都为1的矩阵单元矩阵初始化方式1
GLFloat m[] = {
1,0,0,0, //X Column
0,1,0,0, //Y Column
0,0,1,0, //Z Column
0,0,0,1 // Translation
}
- 单元矩阵初始化方式 2
M3DMatrix44f m = {
1,0,0,0, //X Column
0,1,0,0, //Y Column
0,0,1,0, //Z Column
0,0,0,1 // Translation
}
- 单元矩阵初始化?式3
void m3dLoadIdentity44f(M3DMatrix44f m);
矩阵的乘法
- 矩阵相乘的前提条件:
矩阵1 x 矩阵2
,矩阵1的列 = 矩阵2的行
- 矩阵A x 单元矩阵 = 矩阵A
- 左边:4(行)
4(列)
- 右边:
4(行)
1(列)
满足条件,所以可以做乘法
- 左边:4(行)
1(列)
- 右边:
4(行)
4(列)
不满足条件,结果不可预测
矩阵左乘
在线性代数的维度,坐标的计算,都是按照从左往右顺序进行计算的:
- 变换后的顶点向量 = V_local * M_
model
* M_view
* M_pro
- 变换后的顶点向量 = 顶点 x 模型矩阵 x 观察矩阵 x 投影矩阵
为了方便记忆,我们按照 MVP
的顺序来记
- M:model,表示模型矩阵
- V:view,表示视图矩阵
- P:projection,表示投影矩阵
- 从上图的过程中可以看出,实际计算中,不管多少个顶点,都是两个两个逐个计算的,不会一次性的全部计算
- 上图中的矩阵是行矩阵,不是OpenGL的习惯
OpenGL的习惯——矩阵左乘
我们知道矩阵的乘法是有顺序的,上面的是按照线性代数的习惯,进行的乘法,而且是使用的行矩阵,但是在OpenGL中,我们习惯使用列矩阵,通过相反的顺序 PVM
来进行乘法,过程如下图所示:
- 变换后的顶点向量 = M_
pro
* M_view
* M_model
* V_local - 变换后的顶点向量 = 投影矩阵 x 观察矩阵 x 模型矩阵 x 顶点
OpenGL ES 中的代码片段
(二)理解变换以及模型视图矩阵
投影(projection):将3D数据“压扁”成2D数据的处理过程
前面讲的正投影和透视投影只是变换中的投影变换而已,还有视图变换、模型变换等多种变换类型。如下表:
OpenGL 变换术语概念
变换 | 应 用 |
---|---|
视图 | 指定观察者或照相机的位置 |
模型 | 在场景中移动物体 |
模型视图 | 描述视图和模型变换的二元性 |
投影 | 改变视景体的大小或重新设置它的形状 |
视口 | 这是一种伪变换,只是对窗口上的最终输出进行缩放 |
上面将矩阵的时候就提到过了,有三种矩阵,MVP的名词在变换场景中同样适用
- M:model,表示模型矩阵
- V:view,表示视图矩阵
- P:projection,表示投影矩阵
2.1 视觉坐标
- 无论进行何种变换,我们都可以将它们视为“绝对的”屏幕坐标;
- 视觉坐标表示一个虚拟的固定坐标系,通常作为参考坐标系使用;
- a图:表示我们垂直于手机屏幕视角;
- b图:表示我们的头稍微歪了一点,屏幕里面表示“最深的”地方;
2.2 视图变换(View)
- 透视投影:观察点位于原点(0,0,0),并沿z轴负方向(向显示器内部“看进去”)。观察点相对于视觉坐标系进行移动,来提供特定的有利位置;
- 正投影:观察者被认定在z轴正方向无穷远的位置(上帝视角),能够看到视景体中的任何东西;
- 视图变换:允许我们将观察点放在任何位置,并允许在任何方向上观察场景;
- 确认视图变换:就是确认
观察者的位置
,以及观察者所观察的方向
; -
从大局上考虑,在应用任何其他模型变换之前,必须先应用视图变换
。原因是,对于视觉坐标系而言,视图变换移动了当前的工作坐标系,所有后续变换都会基于新调整的坐标系进行,所以视图变换应该在其他所有的变换之前。
2.3 模型变换(Model)
常见的模型变换有三种
- 平移:Translation
- 旋转:Rotation
- 缩放:Scaling
- 参数1:结果矩阵,将平移后的结果放到这个矩阵中;
- 参数234:xyz轴上的移动距离;
void m3dTranslationMatrix44(M3DMatrix44f m, floata x, float y, float z);
- 参数1:弧度,m3dDegToRad():度数转弧度;
- 参数234:选择围绕哪个轴旋转,如果沿x轴旋转,则为(1,0,0),如果有其他组合轴,以具体需求来设定xyz之间的关系;
m3dRotationMatrix44(m3dDegToRad(45.0), floata x, float y, float z);
- 参数1:结果矩阵,将缩放后的结果放到这个矩阵中;
- 参数234:xyz轴上的缩放因子;
- 翻转:如果沿某个轴缩放-1,则表示在该轴翻转,例如
Xa x -1 = -Xa
void m3dScaleMatrix44(M3DMatrix44f m, floata xScale, float yScale, float zScale);
- 不同顺序的变换结果是不一样的
-
矩阵a x 矩阵b
与矩阵b x 矩阵a
结果不一样,矩阵部分讲过了 - 上图中:先旋转后平移 和 先平移后旋转的结果不一样
综合变换公式(矩阵乘法)
void m3dMatrixMultiply44(M3DMatrix44f product, const M3DMatrix44f a, const M3DMatrix44f b);
- 参数1:结果矩阵,(注意两个矩阵的顺序,这个说了很多回了)
- 参数2:目标矩阵1
- 参数3:目标矩阵2
2.4 模型视图的二元性(ModelView)
- 视图和模型变换的结果其实是一样的,区分开是为了程序员的方便;(“后视镜里的世界,越来越远的道别”——周杰伦《一路向北》)
-
模型视图:值这两种变换在变换管线中进行组合,成为一个单独的矩阵,即模型视图矩阵
; - 使用视图变换的原因,在绘制对象之前应用到一个虚拟对象(观察者)之上的模型变换。在加入更多对象到场景之后,还会指定更多新的变换。
初始变换(视图变换)是所有其他变换参考的基础。
2.5 投影变换(Projection)
2.6 视口变换
- 二维投影:上面所有变化完成之后的一个结果;
- 视口变换:将二维投影映射到物理窗口坐标的变换,颜色缓冲区和窗口像素存在一一对应的关系;
- 图形硬件自动完成,不需要我们手动处理。
(三)模型视图矩阵案例
3.1 案例一:正方形的旋转移动
//类型
GLMatrixStack::GLMatrixStack(int iStackDepth = 64);
//在堆栈顶部载?一个单元矩阵
void GLMatrixStack::LoadIdentity(void);
//在堆栈顶部载入任何矩阵
//参数:4*4矩阵
void GLMatrixStack::LoadMatrix(const M3DMatrix44f m);
//矩阵乘以矩阵堆栈顶部矩阵,相乘结果存储到堆栈的顶部
void GLMatrixStack::MultMatrix(const M3DMatrix44f);
//获取矩阵堆栈顶部的值 GetMatrix 函数
//为了适应GLShaderMananger的使?用,或者获取顶部矩阵的副本
const M3DMatrix44f & GLMatrixStack::GetMatrix(void);
void GLMatrixStack::GetMatrix(M3DMatrix44f mMatrix);
3.2 案例二:使用三角形批次类创建球体、花托、圆柱(锥)、磁盘
这些图形都有些什么特点?显而易见,都是用三角形构成的。怎么通过三角形来构成这些图形呢,这些就不是我们操心的东西了,很多OpenGL的先驱者已经完成了这些特殊形状的绘制,我们只需要调用接口即可。——“给我一个三角形批次类,我还你世界万物”
关于移动、深度测试、边框、混合、背面消除等技术,这里就不做过多解释,前面的文章已经介绍的非常清楚了。
定义一组批次类
//球
GLTriangleBatch sphereBatch;
//环
GLTriangleBatch torusBatch;
//圆柱
GLTriangleBatch cylinderBatch;
//锥
GLTriangleBatch coneBatch;
//磁盘
GLTriangleBatch diskBatch;
1. 绘制球体
/*
gltMakeSphere(GLTriangleBatch& sphereBatch, GLfloat fRadius, GLint iSlices, GLint iStacks);
参数1:sphereBatch,三角形批次类对象
参数2:fRadius,球体半径
参数3:iSlices,从球体底部堆叠到顶部的三角形带的数量;其实球体是一圈一圈三角形带组成
参数4:iStacks,围绕球体一圈排列的三角形对数
建议:一个对称性较好的球体的片段数量是堆叠数量的2倍,就是iStacks = 2 * iSlices;
绘制球体都是围绕Z轴,这样+z就是球体的顶点,-z就是球体的底部。
*/
gltMakeSphere(sphereBatch, 3.0, 10, 20);
2. 绘制花托
/*
gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
参数1:torusBatch,三角形批次类对象
参数2:majorRadius,甜甜圈中心到外边缘的半径
参数3:minorRadius,甜甜圈中心到内边缘的半径
参数4:numMajor,沿着主半径的三角形数量
参数5:numMinor,沿着内部较小半径的三角形数量
*/
gltMakeTorus(torusBatch, 3.0f, 0.75f, 15, 15);
3. 绘制圆柱
/*
void gltMakeCylinder(GLTriangleBatch& cylinderBatch, GLfloat baseRadius, GLfloat topRadius, GLfloat fLength, GLint numSlices, GLint numStacks);
参数1:cylinderBatch,三角形批次类对象
参数2:baseRadius,底部半径
参数3:topRadius,头部半径
参数4:fLength,圆形长度
参数5:numSlices,围绕Z轴的三角形对的数量
参数6:numStacks,圆柱底部堆叠到顶部圆环的三角形数量
*/
4. 绘制圆柱
圆锥其实就是没有顶部的圆柱,所以还是使用同一个批次类,只是第2个参数不一样而已
/*
void gltMakeCylinder(GLTriangleBatch& cylinderBatch, GLfloat baseRadius, GLfloat topRadius, GLfloat fLength, GLint numSlices, GLint numStacks);
参数1:cylinderBatch,三角形批次类对象
参数2:baseRadius,底部半径
参数3:topRadius,头部半径
参数4:fLength,圆形长度
参数5:numSlices,围绕Z轴的三角形对的数量
参数6:numStacks,圆柱底部堆叠到顶部圆环的三角形数量
*/
//圆柱体,从0开始向Z轴正方向延伸。
//圆锥体,是一端的半径为0,另一端半径可指定。
gltMakeCylinder(coneBatch, 2.0f, 0.0f, 3.0f, 13, 2);
5. 绘制磁盘
/*
void gltMakeDisk(GLTriangleBatch& diskBatch, GLfloat innerRadius, GLfloat outerRadius, GLint nSlices, GLint nStacks);
参数1:diskBatch,三角形批次类对象
参数2:innerRadius,内圆半径
参数3:outerRadius,外圆半径
参数4:nSlices,圆盘围绕Z轴的三角形对的数量
参数5:nStacks,圆盘外网到内围的三角形数量
*/
(四)变换管线
4.1 矩阵堆栈
矩阵堆栈的作用
矩阵堆栈的作用是对矩阵的构造和管理提供便捷,矩阵堆栈有以下一些特性:
- 默认的堆栈深度是64
- 矩阵堆栈初始化时,在堆栈中默认保存了单元矩阵
- 初始化单元矩阵:
m3dLoadIdentity44
方法的源码并没有找到。注释说实现在Math3d.cpp
文件中,我们并没有获取到这个文件,所以猜测它的实现原理应该跟下面的LoadMatrix
类似
inline void LoadIdentity(void) {
m3dLoadIdentity44(pStack[stackPointer]);
}
// LoadIdentity
// Implemented in Math3d.cpp
void m3dLoadIdentity33(M3DMatrix33f m);
void m3dLoadIdentity33(M3DMatrix33d m);
void m3dLoadIdentity44(M3DMatrix44f m);
void m3dLoadIdentity44(M3DMatrix44d m);
- 加载一个矩阵:从源码中可以看出,加载一个矩阵,实际上就是将一个矩阵放入当前堆栈的栈顶位置。跟压栈不一样的地方是,压栈是在栈顶上方新压入一个矩阵,而load是修改当前栈顶的矩阵
inline void LoadMatrix(const M3DMatrix44f mMatrix) {
m3dCopyMatrix44(pStack[stackPointer], mMatrix);
}
inline void m3dCopyMatrix44(M3DMatrix44d dst, const M3DMatrix44d src)
{ memcpy(dst, src, sizeof(M3DMatrix44d)); }
矩阵堆栈的操作
压栈和出栈一定是成对出现
压栈和出栈一定是成对出现
压栈和出栈一定是成对出现
- 压栈:复制当前矩阵值,将结果放在堆栈顶部
void PushMatrix(const M3DMatrix44f mMatrix) {
if(stackPointer < stackDepth) {
stackPointer++;
// 复制当前矩阵,存入栈顶
m3dCopyMatrix44(pStack[stackPointer], mMatrix);
}
else
lastError = GLT_STACK_OVERFLOW;
}
- 出栈:移除顶部矩阵,恢复它下面的值
inline void PopMatrix(void) {
if(stackPointer > 0)
stackPointer--;
else
lastError = GLT_STACK_UNDERFLOW;
}
- 乘法:获取栈顶矩阵,与目标矩阵相乘,将结果矩阵放入栈顶
inline void MultMatrix(const M3DMatrix44f mMatrix) {
M3DMatrix44f mTemp;
m3dCopyMatrix44(mTemp, pStack[stackPointer]);
m3dMatrixMultiply44(pStack[stackPointer], mTemp, mMatrix);
}
- 获取矩阵:两种方式,根据具体需求进行选择使用
- 一种是带返回值,直接返回栈顶矩阵:但是外部矩阵(变量)不能被修改。如果在外面修改的堆栈中的矩阵,可能会造成意想不到的结果;
- 拷贝栈顶矩阵到传进来的参数地址:因为这里是拷贝了一份新的矩阵,所以不用担心外面的修改会影响到堆栈里面的矩阵。
// Two different ways to get the matrix
const M3DMatrix44f& GetMatrix(void) { return pStack[stackPointer]; }
void GetMatrix(M3DMatrix44f mMatrix) { m3dCopyMatrix44(mMatrix, pStack[stackPointer]); }
矩阵堆栈的仿射变换
矩阵堆栈处理提供管理堆栈的功能,还提供了对创建旋转、平移、缩放矩阵的支持
- 通过API获取仿射变换需要的数据;
- 创建仿射变换矩阵;
- 获取栈顶矩阵;
- 将仿射变换矩阵和栈顶矩阵相乘的结果放入栈顶
这三个内建API的过程也是我们自己在使用矩阵堆栈的时候需要用到的
// 缩放
void Scale(GLfloat x, GLfloat y, GLfloat z) {
M3DMatrix44f mTemp, mScale;
m3dScaleMatrix44(mScale, x, y, z);
m3dCopyMatrix44(mTemp, pStack[stackPointer]);
m3dMatrixMultiply44(pStack[stackPointer], mTemp, mScale);
}
// 平移
void Translate(GLfloat x, GLfloat y, GLfloat z) {
M3DMatrix44f mTemp, mScale;
m3dTranslationMatrix44(mScale, x, y, z);
m3dCopyMatrix44(mTemp, pStack[stackPointer]);
m3dMatrixMultiply44(pStack[stackPointer], mTemp, mScale);
}
// 旋转
void Rotate(GLfloat angle, GLfloat x, GLfloat y, GLfloat z) {
M3DMatrix44f mTemp, mRotate;
m3dRotationMatrix44(mRotate, float(m3dDegToRad(angle)), x, y, z);
m3dCopyMatrix44(mTemp, pStack[stackPointer]);
m3dMatrixMultiply44(pStack[stackPointer], mTemp, mRotate);
}
4.2 管理管线
管线的作用就是记录模式视图(MV)矩阵和投影(P)矩阵,在获取MVP(模式视图投影)矩阵时,将MV矩阵和P矩阵相乘的结果返回。这么做有什么好处呢?
-
标准化
:我们上面讲过了,矩阵的乘法是有顺序的,而MV矩阵和P矩阵哪个在前哪个在后,如果是我们手写的矩阵乘法的时候,难免会出现错误。而在管线里面,它将会以一个固定的顺序来做乘法,外部是不需要知道实现细节的。具体的源码实现,后面会讲。 -
封装性
:如果我们每次在使用MVP矩阵的时候,都手动去做乘法,这就太不程序员了。
管线源码解读
作为一个程序员,从源码的角度来理解管线。
- 管线API:真的是异常的简单
- 三个setter方法
- 四个getter方法
-
setter方法
:跟一般setter方法一样,就就是保存一个地址
inline void SetModelViewMatrixStack(GLMatrixStack& mModelView) { _mModelView = &mModelView; }
inline void SetProjectionMatrixStack(GLMatrixStack& mProjection) { _mProjection = &mProjection; }
inline void SetMatrixStacks(GLMatrixStack& mModelView, GLMatrixStack& mProjection) {
_mModelView = &mModelView;
_mProjection = &mProjection;
}
管线管理啥,只管理 模型视图矩阵(MV)和 投影矩阵(P)
,因为接口里面只提供了这两个矩阵的设置入口??????
getter方法
const M3DMatrix44f& GetModelViewProjectionMatrix(void)
{
m3dMatrixMultiply44(_mModelViewProjection, _mProjection->GetMatrix(), _mModelView->GetMatrix());
return _mModelViewProjection;
}
inline const M3DMatrix44f& GetModelViewMatrix(void) { return _mModelView->GetMatrix(); }
inline const M3DMatrix44f& GetProjectionMatrix(void) { return _mProjection->GetMatrix(); }
const M3DMatrix33f& GetNormalMatrix(bool bNormalize = false)
{
m3dExtractRotationMatrix33(_mNormalMatrix, GetModelViewMatrix());
if(bNormalize) {
m3dNormalizeVector3(&_mNormalMatrix[0]);
m3dNormalizeVector3(&_mNormalMatrix[3]);
m3dNormalizeVector3(&_mNormalMatrix[6]);
}
return _mNormalMatrix;
}
其实管线的 getter 方法只有两个,中间两个是 inline 方法,并不支持外部调用。
GetNormalMatrix
这个方法,暂时还没用到,这里不做解释,那就只剩下一个方法了。
GetModelViewProjectionMatrix
GetModelViewProjectionMatrix
GetModelViewProjectionMatrix
一般重要的东西,至少要默写三遍,就是这个MVP矩阵。
如果你还有印象,我们前面在介绍着色器的时候,平面着色器和上色着色器中是不是有一个参数就是MVP矩阵。管线就是为了给我们提供这个边界,不需要我们每次都手动去做乘法,它直接提供了MVP的结果给我们。
学习!记录!知识总是环环相扣!前面的困惑在后面的学习中总会柳暗花明又一村!
4.3 照相机(观察者)
角色?。篏LFrame
这个类型是GLTools库中的一种数据结构,只在OpenGL阶段使用,在后面的OpenGL ES阶段中边不在使用了,所以这里就不做过多的介绍。
protected:
M3DVector3f vOrigin; // Where am I?
M3DVector3f vForward; // Where am I going?
M3DVector3f vUp; // Which way is up?
- vOrigin:对应x轴
- vForward:对应z轴
- vUp:对应y轴
GLFrame的操作
源码很长,也看不懂??,他是怎么转换的,我们就不用去探索了,知道怎么用就行。
- 获取矩阵:
GetMatrix
,结果是一般矩阵,对应世界坐标系
//4.创建矩阵mObjectFrame
M3DMatrix44f mObjectFrame;
//从ObjectFrame 获取矩阵到mOjectFrame中
objectFrame.GetMatrix(mObjectFrame);
//将modelViewMatrix 的堆栈中的矩阵 与 mOjbectFrame 矩阵相乘,存储到modelViewMatrix矩阵堆栈中
modelViewMatrix.MultMatrix(mObjectFrame);
- 获取照相机矩阵:
GetCameraMatrix
,结果对应的是观察者坐标系
//3.获取摄像头矩阵
M3DMatrix44f mCamera;
//从camereaFrame中获取矩阵到mCamera
cameraFrame.GetCameraMatrix(mCamera);
//模型视图堆栈的 矩阵与mCamera矩阵 相乘之后,存储到modelViewMatrix矩阵堆栈中
modelViewMatrix.MultMatrix(mCamera);
照相机(观察者)
这只是一个逻辑存在的事物,在实际中并不存在,为什么这么说呢?
它本质是啥,就是一个矩阵。那为什么这个矩阵要叫观察者呢?
当我们在使用这个矩阵的时候,它产生的变化效果,看起来是从观察者角度去移动的。所以称它为观察者。
果然学习总是环环相扣,前面不理解不要紧,后面自然就会懂了。
4.4 变换流程
前面三小节,分别从代码和抽象的角度对某些现象进行了解释。如果是认真的看到这里,那下面这个流程图,不需要记,自然会刻在你的脑海里。
以下面这段代码为例
// 1.模型视图矩阵栈堆,压栈
modelViewMatrix.PushMatrix();
// 2.获取摄像头矩阵
M3DMatrix44f mCamera;
//从camereaFrame中获取矩阵到mCamera
cameraFrame.GetCameraMatrix(mCamera);
//模型视图堆栈的 矩阵与mCamera矩阵 相乘之后,存储到modelViewMatrix矩阵堆栈中
modelViewMatrix.MultMatrix(mCamera);
// 3.创建矩阵mObjectFrame
M3DMatrix44f mObjectFrame;
//从ObjectFrame 获取矩阵到mOjectFrame中
objectFrame.GetMatrix(mObjectFrame);
//将modelViewMatrix 的堆栈中的矩阵 与 mOjbectFrame 矩阵相乘,存储到modelViewMatrix矩阵堆栈中
modelViewMatrix.MultMatrix(mObjectFrame);
//4. 使用矩阵
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
//5. pop
modelViewMatrix.PopMatrix();
认真读完上面关于矩阵堆栈的操作,这个流程图相对来说就是非常简单了。核心是每一步操作会后矩阵堆栈中栈顶矩阵的变换
。