前言
首先附上一个我个人花了点时间整理的Demo,其中包含了绘制各基本图形、甜甜圈的代码。
一、向量和矩阵
1、向量
什么是向量,向量是一个有长度(模)和方向的有向线段。
在OpenGL中,它就是一个顶点,也就是一个向量。
1.1 向量与标量
标量其实可以理解成数字,它强调的是数值;而向量强调的长度和方向。
1.2 单位向量
长度为1的向量即为单位向量。
而对于一些只关心方向不关心大小的向量,使用单元向量会比较方便。这个时候我们可以对非单位且非0向量进行标准化,将其转换为单位向量。
1.3 向量的写法
通常,垂直书写的是列向量;水平书写的是行向量。
1.4 OpenGL中使用向量
在OpenGL中,可以使用math3d库来定义向量,这个库提供了两个关于向量的数据类型。分别是 M3DVector3f:(x,y,z)和M3DVector4f:(x,y,z,w)。
w是缩放因子,x、y、z可以通过除以w来做到缩放。一般情况下,w坐标值为1。
//三维向量/四维向量的声明
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
};
1.5 向量点乘
1.5.1 数学意义
向量点乘的优先级高于加法和减法,两个向量点乘即为其对应分量乘积相加的结果,其结果一个标量。
如 [a1,a2]·[b1,b2] = a1 * b1 + a2 * b2。
1.5.2 几何意义
点乘的结果在几何上表示两个向量的接近程度,点乘的结果越大,两个向量越接近。
a · b = |a||b|*cosθ,θ为两个向量之间的夹角,由此也可以看出,点乘是满足交换律的。
在OpenGL中,math3d 库中提供了了关于点乘的API
//1.m3dDotProduct3 函数获得2个向量之间的点乘结果;
float m3dDotProduct3(const M3DVector3f u,const M3DVector3f v);
//2.m3dGetAngleBetweenVector3 即可获取2个向量之间夹角的弧度值;
float m3dGetAngleBetweenVector3(const M3DVector3f u,const M3DVector3f v);
1.6 向量叉乘
向量叉乘的优先级也高于加法和减法,它的结果是一个向量(如上图)且不满足交换律。这个向量垂直于两个向量,这个结果向量可以称之为a向量与b向量所在平面的法线。
在OpenGL中,math3d 库中提供了关于叉乘的API
//1.m3dCrossProduct3 函数获得2个向量之间的叉乘结果得到一个新的向量
void m3dCrossProduct3(M3DVector3f result,const M3DVector3f u ,const M3DVector3f v);
2、矩阵
一个矩阵可以看做是若干个向量组成的。在OpenGL中,常常用它来做各种变换。如平移、缩放等。
在之前的OpenGL例子中,首先我们会在setupRC()方法中使用各种方式定义很多顶点数据,复制到批次类中,如果不做任何操作,直接渲染的话,那得到的就是一个普通图形。若我们需要对图形做各种变换的操作,则需要通过各种矩阵进行计算。OpenGL代码如下图。
void RenderScene(void)
{
...
//压栈
modelViewMatrix.PushMatrix();
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);
//矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后简存储在堆栈的顶部
modelViewMatrix.MultMatrix(mCamera);
M3DMatrix44f mObjectFrame;
//只要使用 GetMatrix 函数就可以获取矩阵堆栈顶部的值,这个函数可以进行2次重载。用来使用GLShaderManager 的使用。或者是获取顶部矩阵的顶点副本数据
objectFrame.GetMatrix(mObjectFrame);
//矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后简存储在堆栈的顶部
modelViewMatrix.MultMatrix(mObjectFrame);
...
}
1、矩阵的定义
在OpenGL中,math3d库也提供了两个数据类型来定义矩阵,分别是 M3DMatrix33f[9]和M3DMatrix44f[16]。
与其他变成标准不同的是,在OpenGL中,更倾向于使用一维数组来定义矩阵。原因是因为OpenGL使用的是 Column-Major(以列为主)矩阵排序的约定。
2、列矩阵
上图的矩阵为列矩阵,如图所示,这个矩阵的每一列都是一个由四个元素组成的向量。事实上,这样的一个矩阵可以确定空间中的一个位置。
当我们在OpenGL编程的时候,将一个对象的所有顶点数据乘以这个向量,即可让这个对象变化到空间中矩阵指向的位置和方向。
2、单元矩阵
对角线元素都为1的矩阵即为单元矩阵。单元矩阵具有以下特点
- 任何矩阵乘以单元矩阵都等于原来的矩阵,相当于任何数字乘以1都等于原来的数字;
- 单元矩阵满足交换律,任何矩阵乘以单元矩阵等于单元矩阵乘以对应矩阵。
二、变换
在之前的几篇文章中,很多都有涉及到从物体坐标转换到屏幕坐标的各种变换。如下图:
从物体坐标变换到裁剪坐标的过程是在顶点着色器中完成的,在这过程中,物体坐标先后经过了模型变换、视图变换、投影变换。而这些变换本质上其实就是矩阵的相乘。
在OpenGL的维度中,变换顶点向量的过程如下:
变换后顶点向量 = 投影矩阵 ✖️ 视图变换矩阵 ✖️ 模型矩阵 ✖️ 顶点
而在线性代数的维度,一般的坐标计算都是从左往右的顺序进行计算的。如下:
变换后顶点向量 = 顶点 ✖️ 模型矩阵 ✖️ 视图变换矩阵 ✖️ 投影矩阵
那么为什么在OpenGL维度与线性代数维度是相反的读法呢。
1、行向量和列向量的变换
由于向量实际上是某一维度为1的矩阵,那么根据矩阵乘法的规则,会出现下面的情况:
- 行向量左乘矩阵
在图形学中一般矩阵都是4x4的,行向量一般设置为1x4的矩阵(齐次坐标)。当行向量左乘矩阵的时候 (1x4)* (4x4)得到的是一个1x4的行向量 - 行向量右乘矩阵
这种情况,也就是(4x4)*(1x4),根据矩阵相乘的规则,这是不允许的 - 列向量左乘矩阵
这种情况,也就是(4x1)*(4x4),根据矩阵相乘规则,这也是不允许的 - 列向量右乘矩阵
这种情况,也就是(4x4)*(4x1),得到的是一个4x1的行向量
根据上述规则,可以得出,如果我们需要做各种变换,在计算的时候,矩阵之间相乘只能使用行向量左乘矩阵和列向量右乘矩阵,这是矩阵乘法规则的限制。
而在OpenGL中,采取的是列向量,因此,我们使用的是列向量右乘矩阵的方式,也就是上图中 (4x4)(4x4)(4x4)*(4x1)的变换过程。
2、行向量和列向量在编程语言中的内存布局
假设有一个4x4的矩阵M,我们现在有一个顶点的坐标是V,通过M矩阵的变换可以把它变为VE,现在分别假设 V是行向量或者V是列向量,于是有以下两种情形:
(1)V是行向量 , 那么 VE = VM
(2)V是列向量, 那么 VE = MV
举一个实际的例子来看一下。如图
在内存中,对于一个行矩阵,它的排列是[1,2,3,4,5,...,15,16],而对于列矩阵,它在内存中的排列是[1,5,9,13,2,6,...,12,16],仅此而已。从图中可以看出,两种 向量与矩阵的乘积的结果,其实互为转置。
回到OpenGL变换中,在OpenGL采用的列向量的方式,所以它是右乘的计算方式。因此,在代码中,我们一般看到的书写顺序是 先单元矩阵复制一份压栈,然后将栈顶的矩阵取出,乘以观察者矩阵,得到的结果赋值给栈顶,再乘以模型矩阵,得到的结果赋值给栈顶,最后,将投影矩阵乘以栈顶矩阵,最终得到的就是变换空间位置后的顶点的空间位置。
但是,这仅仅是因为,在OpenGL中是列向量的形式,所以采取了右乘的方式计算,这与上述的坐标系变换过程并不冲突。
//压入栈操作,投影矩阵*观察者矩阵*模型视图矩阵
void publicPushStackOperation() {
//复制一份单元矩阵
modelViewStack.PushMatrix();
M3DMatrix44f cameraMatrix;
cameraFrame.GetCameraMatrix(cameraMatrix);
modelViewStack.MultMatrix(cameraMatrix);
M3DMatrix44f objectMatrix;
objectFrame.GetMatrix(objectMatrix);
modelViewStack.MultMatrix(objectMatrix);
//shaderManger. transformPipleLine.GetModelViewProjectionMatrix 这行代码的底层实现,
//其实是m3dMatrixMultiply44(_mModelViewProjection, _mProjection->GetMatrix(), _mModelView->GetMatrix());
//也就是 投影矩阵*观察者矩阵*模型视图矩阵
UseStockShader(GLT_SHADER_FLAT,transformPipleLine.GetModelViewProjectionMatrix(),vBlue);
}
2、各种OpenGL变换
2.1 视图变换
视图变换,其实主要是指定观察者的位置,它是物体应用到场景中的第一种变换,它⽤来确定场景中的有利位置,在默认情况下, 透视
投影中位于原点(0,0,0),并沿着 z 轴负⽅向进⾏观察 (向显示器内部”看过去”)。
从⼤局上考虑, 在应⽤任何其他模型变换之前, 必须先应⽤视图变换. 这样做是因为, 对于视觉坐标系⽽⾔, 视图变换移动了当前的⼯作的坐标系; 后续的变化都会基于新调整的坐标系进⾏。
2.2 模型变换
⽤于操纵模型的某种特定变换.。这些变换通过旋转,缩放,平移将对象移动到需要的位置。
2.3 投影变换
在OpenGL中,投影变换的目的就是得到一个取景体积(视景体),其作用有
- 确定物体投影方式是正投影还是透视投影;
- 确定从图像上裁剪掉哪个物体或者物体的哪一部分。