OpenGL ES 3.0(四)矩阵变换

1、概述

前面几篇关于OpenGLES的文章:

OpenGL ES 2.0 显示图形(上)

OpenGL ES 2.0 显示图形(下)

OpenGL ES 3.0(一)综述

OpenGL ES 3.0(二)GLSL与着色器

OpenGL ES 3.0(三)纹理

有讨论到关于图像的变换和移动,前面这些变换是图像基于每一帧改变物体的顶点并且重配置缓冲区从而使它们移动,但这太繁琐了,而且会消耗很多的处理时间。现在有一个更好的解决方案,使用(多个)矩阵(Matrix)对象可以更好的变换(Transform)一个物体。

矩阵是一种非常有用的数学工具,如果上过线性代数这门课的话应该都会有所了解。这篇主要讨论一下在图像处理中一些最简单的矩阵变换,并且在OpenGL ES中进行实践。

2、向量的乘

要讨论矩阵,一定绕不开向量。向量可以说是最简单的矩阵,可以把它理解为1n的矩阵或者是n1的矩阵,关于向量的加减运算和长度计算等实在太过基础这边不再讨论。这边关于向量的运算主要讨论其乘法相关的运算。

2.1 点乘

向量的点乘一般用 v · k 这样表示,两个向量的点乘结果等于它们的数乘结果再乘两个向量之间夹角的余弦值。公式如下:

点乘公式

等式右边||v|| 和 ||k|| 分别代表两个向量的长度。通过上面的公式也可以反推出两个向量之间的夹角。而对于具体的两个向量,其点乘就是对应位置上的数字相乘再进行累加。

向量点乘

2.2 叉乘

叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量。下图展示了3D空间中叉乘的样子:

3D空间中的叉乘

叉乘公式如下:

向量叉乘

3、矩阵

上面讨论的向量其实也是矩阵的一种,矩阵就是数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。下面是一个2×3矩阵的例子:

2*3矩阵

矩阵可以通过(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫做2×3矩阵的原因。获取上面4的索引是(2, 1)(第二行,第一列)(注:如果是图像索引应该是(1, 2),先算列,再算行)。

下面讨论一下一些矩阵的基本运算。

3.1 矩阵加减

矩阵的加减是矩阵最简单的操作,矩阵和标量进行加减其标量值要加减到矩阵每一个值当中。矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,不过在相同索引下的元素才能进行运算。这也就是说加法和减法只对同维度的矩阵才是有定义的。

矩阵与标量加法
矩阵之间减法

3.2 矩阵数乘

矩阵与标量相乘和矩阵与标量的加减一样,会对每个数据进行乘。所以一般也用矩阵的数乘来缩放矩阵。

矩阵数乘

3.3 矩阵之间相乘

矩阵之间相乘会复杂许多,并且会有一些限制:

**① **只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。

**② **矩阵相乘不遵守交换律,也就是说A ·B 和 B·A 并不相等。

矩阵相乘时是将左边矩阵的i行和右边矩阵的j列分别对应相乘并相加得到新矩阵i行j列位置上的值。结果矩阵的维度是(n, m),n等于左侧矩阵的行数,m等于右侧矩阵的列数。

矩阵之间相乘

4、矩阵变换

前面已经讨论了矩阵和向量的一些基本操作,接下来讨论通过这些操作来对矩阵进行一些常见的变换。

4.1 缩放

对一个向量进行缩放(Scaling)就是对向量的长度进行缩放,而保持它的方向不变。由于进行的是2维或3维操作,可以分别定义一个有2或3个缩放变量的向量,每个变量缩放一个轴(x、y或z)。

先来尝试缩放向量v = (3,2)。可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;将沿着y轴把向量的高度缩放为原来的两倍。最终可得到如下向量s = (1.5,4):

向量缩放

OpenGL ES通常是在3D空间进行操作的,对于2D的情况可以把z轴缩放1倍,就相当于不缩放z轴。上图的缩放操作是不均匀缩放,因为每个轴的缩放因子都不一样。如果每个轴的缩放因子都一样那么就叫均匀缩放。

下面构造一个变换矩阵来提供缩放功能。从单位矩阵(矩阵正斜对角线为1,其余为0的矩阵),每个对角线元素会分别与向量的对应元素相乘,而不会干扰其他维度上的向量值。对于需要将任意向量(x,y,x)分别缩放(S1,S2,S3)倍时,可以用这样的特性来构造缩放矩阵。如下:

缩放矩阵

第四个缩放向量仍然是1,因为在3D空间中缩放w分量是无意义的。w分量另有其他用途,会在后面讨论。

4.2 位移

位移是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上移动了原始向量。和缩放矩阵一样,在4×4矩阵上有几个特别的位置用来执行特定的操作,对于位移来说它们是第四列最上面的3个值。如果我们把位移向量表示为(Tx,Ty,Tz),就能把位移矩阵定义为:

位移矩阵

因为所有的位移值都要乘以向量的w行,所以位移值会加到向量的原始值上。而如果用3x3矩阵位移值就没地方放也没地方乘了,所以是不行的。这也是为什么3维的向量要用四维变换矩阵进行。

这个向量的w分量也叫齐次坐标分量。想要从齐次向量得到3维向量,可以把x、y和z坐标分别除以w坐标。通常不会注意这个问题,因为w分量通常是1.0。使用齐次坐标的好处在于它允许在3D向量上进行位移(如果没有w分量是不能位移向量的)。如果一个向量的齐次坐标分量值是0,这个坐标就是方向向量,因为w坐标是0,这个向量就不能位移。有了位移矩阵就可以在3个方向(x、y、z)上移动物体。

4.3 旋转

上面几个的变换内容相对容易理解,在二维或三维空间中也容易表示出来,但旋转稍复杂些。

首先来定义一个向量的旋转到底是什么。二维或三维空间中的旋转用角来表示。角可以是角度制或弧度制的,周角是360角度或2 PI弧度。下图中展示的二维向量v是由k向右旋转72度所得的:

二维向量的旋转

在三维空间中旋转需要定义一个角和一个旋转轴。物体会沿着给定的旋转轴旋转特定角度。当二维向量在三维空间中旋转时,一般把旋转轴设为z轴。

给定一个角度,可以把一个向量变换为一个经过旋转的新向量。这通常是使用一系列正弦和余弦函数的各种巧妙的组合得到的。旋转矩阵在三维空间中每个单位轴都有不同定义,旋转角度用θ表示:

沿x轴旋转:

沿x轴的旋转矩阵

沿y轴旋转:

沿y轴的旋转矩阵

沿z轴旋转:

沿z轴的旋转矩阵

利用旋转矩阵可以把任意位置向量沿一个单位旋转轴进行旋转。也可以将多个矩阵复合,比如先沿着x轴旋转再沿着y轴旋转。但是这会很快导致一个问题——万向节死锁。对于三维空间中的旋转,一个更好的模型是沿着任意的一个轴旋转。这样的一个旋转矩阵是存在的,但其非常复杂这边不进行展开讨论。

4.4 组合变换

使用矩阵进行变换的真正厉害之处在于,根据矩阵之间的乘法,可以把多个变换组合到一个矩阵中。假设有一个顶点(x, y, z),希望将其缩放2倍,然后位移(1, 2, 3)个单位。需要一个位移和缩放矩阵来完成这些变换。其最终的组合变换矩阵如下:

组合变换矩阵

当矩阵相乘时先写位移再写缩放变换。矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,所以应该从右向左读这个乘法。建议在设计组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会互相影响。比如,如果先位移再缩放,位移的向量也会同样被缩放。

用最终的变换矩阵左乘我们的向量会得到以下结果:

组合变换矩阵效果

如上所示,其目标向量确实放大来两倍并进行了位移。

5、OpenGLES中的矩阵

前面已经讨论完了矩阵的基本知识及操作,接下来讨论下关于OpenGLES 中矩阵的应用。OpenGLES中有一个类是专门用来进行矩阵处理的android.opengl.Matrix。这个类里面包括了对于矩阵的各种前面提到的处理变换操作。

**① **Matrix.setIdentityM() :用来创建一个单位矩阵。其中第一个参数是创建出来的单位矩阵存储的地方,是一个float类型的一维数组。第二个参数是存储的数据位置的偏移量,也就是说从哪里开始存储。生成的结果先按照列优先存储的,也就是说先存放第一列的数据,再存放第二列的数据,以此类推。前面理论部分已经提到,所有变换都是基于单位矩阵的基础上进行的,所以第一步创建单位矩阵是必须的。

**② **Matrix.rotateM() :用来进行旋转变换的。第一个参数是需要变换的矩阵;第二参数是偏移量;第三个参数是旋转角度,这边是以角度制,也就是说是0-360这个范围;第四、五、六个参数分别代表旋转轴向量的x,y,z值。如果x=0,y=0,z = 1 就相当于以z轴为旋转轴进行旋转,其他类似。

旋转90度效果

**③ **Matrix.translateM() :用来进行图像的位移,第一个参数是需要变换的矩阵;第二个参数是偏移量;第三、四、五个参数分别对应x,y,z 方向的位移量。其以图像自身x,y,z方向为单位,也就是说当x方向位移量为0.5时,相当于向右移动0.5个身位,其他类似。

偏移x=0.5 y=0.5效果

**④ **Matrix.scaleM():用来进行图像的缩放,第一个参数是需要变换的矩阵;第三、四、五个参数分别对应x,y,z 方向的缩放比例,当x方向缩放为0.5时,相当于向x方向缩放为原来的0.5倍,其他类似。

缩放x=2 y=0.5 效果

前面说过这些变换是可以进行组合运算的,其组合代码如下:


Triangle.kt

init{

    ...

    Matrix.setIdentityM(mTransMatrix, 0)

    Matrix.scaleM(mTransMatrix,0,2f,0.5f,1f)

    Matrix.rotateM(mTransMatrix, 0, mAngle, 0f, 0f, 1.0f)

    Matrix.translateM(mTransMatrix, 0, 0.5f, 0.5f, 0f)

    ...

}

之间用上面的操作相当于生成了一个组合矩阵,其效果如下图:

组合矩阵效果

光生成了变换矩阵还不能完成上述操作,还需要将变换矩阵数据传入到顶点着色器上:


// Triangle.kt

private val vertexShaderCode =

                    ...

                    "uniform mat4 transform;" +

                    "void main() {" +

                    " gl_Position = transform * vec4(aPos, 1.0);" +

                    ...

                    "}"

fun draw() {

...

        val transformLoc = GLES30.glGetUniformLocation(mProgram, "transform")

        GLES30.glUniformMatrix4fv(transformLoc, 1, false, mTransMatrix, 0)

...

}

推荐阅读更多精彩内容