Matrix简介
Matrix是一个矩阵,主要功能是坐标映射,数值转换。
它看起来大概是下面这样:
Matrix作用就是坐标映射,那么为什么需要Matrix呢? 举一个简单的例子:
我的的手机屏幕作为物理设备,其物理坐标系是从左上角开始的,但我们在开发的时候通常不会使用这一坐标系,而是使用内容区的坐标系。
以下图为例,我们的内容区和屏幕坐标系还相差一个通知栏加一个标题栏的距离,所以两者是不重合的,我们在内容区的坐标系中的内容最终绘制的时候肯定要转换为实际的物理坐标系来绘制,Matrix在此处的作用就是转换这些数值。
假设通知栏高度为20像素,导航栏高度为40像素,那么我们在内容区的(0,0)位置绘制一个点,最终就要转化为在实际坐标系中的(0,60)位置绘制一个点。
Matrix特点
作用范围更广,Matrix在View,图片,动画效果等各个方面均有运用,相比与之前讲解等画布操作应用范围更广。
更加灵活,画布操作是对Matrix的封装,Matrix作为更接近底层的东西,必然要比画布操作更加灵活。
封装很好,Matrix本身对各个方法就做了很好的封装,让开发者可以很方便的操作Matrix。
难以深入理解,很难理解中各个数值的意义,以及操作规律,如果不了解矩阵,也很难理解前乘,后乘。
常见误解
1.认为Matrix最下面的一行的三个参数(MPERSP_0、MPERSP_1、MPERSP_2)没有什么太大的作用,在这里只是为了凑数。
实际上最后一行参数在3D变换中有着至关重要的作用,这一点会在后面中Camera一文中详细介绍。
2.最后一个参数MPERSP_2被解释为scale
的确,更改MPERSP_2的值能够达到类似缩放的效果,但这是因为齐次坐标的缘故,并非这个参数的实际功能。
Matrix基本原理
Matrix 是一个矩阵,最根本的作用就是坐标转换,下面我们就看看几种常见变换的原理:
基本变换有4种: 平移(translate)、缩放(scale)、旋转(rotate) 和 错切(skew)。
下面我们看一下四种变换都是由哪些参数控制的。
从上图可以看到最后三个参数是控制透视的,这三个参数主要在3D效果中运用,通常为(0, 0, 1),不在本篇讨论范围内,暂不过多叙述,会在之后对文章中详述其作用。
1.缩放(Scale)
用矩阵表示:
你可能注意到了,我们坐标多了一个1,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的(x, y),两者看起来一样,计算机无法区分,为此让计算机也可以区分它们,增加了一个标志位,增加之后看起来是这样:
(x, y, 1) - 点
(x, y, 0) - 向量
另外,齐次坐标具有等比的性质,(2,3,1)、(4,6,2)…(2N,3N,N)表示的均是(2,3)这一个点。(将MPERSP_2解释为scale这一误解就源于此)。
2.错切(Skew)
错切存在两种特殊错切,水平错切(平行X轴)和垂直错切(平行Y轴)。
水平错切
用矩阵表示:
图例:
垂直错切
用矩阵表示:
图例:
复合错切
水平错切和垂直错切的复合。
用矩阵表示:
图例:
3.旋转(Rotate)
假定一个点 A(x0, y0) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x, y) 如下:
用矩阵表示:
图例:
4.平移(Translate)
此处也是使用齐次坐标的优点体现之一,实际上前面的三个操作使用 2x2 的矩阵也能满足需求,但是使用 2x2 的矩阵,无法将平移操作加入其中,而将坐标扩展为齐次坐标后,将矩阵扩展为 3x3 就可以将算法统一,四种算法均可以使用矩阵乘法完成。
用矩阵表示:
图例:
Matrix复合原理
其实Matrix的多种复合操作都是使用矩阵乘法实现的,从原理上理解很简单,但是,使用矩阵乘法也有其弱点,后面的操作可能会影响到前面到操作,所以在构造Matrix时顺序很重要。
我们常用的四大变换操作,每一种操作在Matrix均有三类,前乘(pre),后乘(post)和设置(set),由于矩阵乘法不满足交换律,所以前乘(pre),后乘(post)和设置(set)的区别还是很大的。
前乘(pre)
前乘相当于矩阵的右乘:
这表示一个矩阵与一个特殊矩阵前乘后构造出结果矩阵。
后乘(post)
后乘相当于矩阵的左乘:
这表示一个矩阵与一个特殊矩阵后乘后构造出结果矩阵。
设置(set)
设置使用的不是矩阵乘法,而是直接覆盖掉原来的数值,所以,使用设置可能会导致之前的操作失效。
组合
错误结论一:pre 是顺序执行,post 是逆序执行。
// 第一段 pre 顺序执行,先平移(T)后旋转(R)
Matrix matrix = new Matrix();
matrix.preTranslate(pivotX,pivotY);
matrix.preRotate(angle);
Log.e("Matrix", matrix.toShortString());
// 第二段 post 逆序执行,先平移(T)后旋转(R)
Matrix matrix = new Matrix();
matrix.postRotate(angle);
matrix.postTranslate(pivotX,pivotY)
Log.e("Matrix", matrix.toShortString());
这两段代码最终结果是等价的,于是轻松证得这个结论的正确性,但事实真是这样么?
首先,从数学角度分析,pre 和 post 就是右乘或者左乘的区别,其次,它们不可能实际影响运算顺序(程序执行顺序)。以上这两段代码等价也仅仅是因为最终化简公式一样而已。
设原始矩阵为 M,平移为 T ,旋转为 R ,单位矩阵为 I ,最终结果为 M’
矩阵乘法不满足交换律,即 A*B ≠ B*A
矩阵乘法满足结合律,即 (A*B)*C = A*(B*C)
矩阵与单位矩阵相乘结果不变,即 A * I = A
由于上面例子中原始矩阵(M)是一个单位矩阵(I),所以可得:
// 第一段 pre
M’ = (MT)R = ITR = T*R
// 第二段 post
M’ = T*(RM) = TRI = TR
由于两者最终的化简公式是相同的,所以两者是等价的,但是,这结论不具备普适性。
即原始矩阵不为单位矩阵的时候,两者无法化简为相同的公式,结果自然也会不同。另外,执行顺序就是程序书写顺序,不存在所谓的正序逆序。
错误结论二:pre 是先执行,而 post 是后执行。
但从严谨的数学和程序角度来分析,完全是不可能的,还是上面所说的,pre 和 post 不能影响程序执行顺序,而程序每执行一条语句都会得出一个确定的结果,所以,它根本不能控制先后执行
如何理解和使用 pre 和 post ?
pre : 右乘, M‘ = M*A
post : 左乘, M’ = A*M
那么如何使用?
正确使用方式就是先构造正常的 Matrix 乘法顺序,之后根据情况使用 pre 和 post 来把这个顺序实现。
还是用一个最简单的例子理解,假设需要围绕某一点旋转。
可以用这个方法 xxxRotate(angle, pivotX, pivotY) ,由于我们这里需要组合构造一个 Matrix,所以不直接使用这个方法。
首先,有两条基本定理:
所有的操作(旋转、平移、缩放、错切)默认都是以坐标原点为基准点的。
之前操作的坐标系状态会保留,并且影响到后续状态。
基于这两条基本定理,我们可以推算出要基于某一个点进行旋转需要如下步骤:
1. 先将坐标系原点移动到指定位置,使用平移 T
2. 对坐标系进行旋转,使用旋转 S (围绕原点旋转)
3. 再将坐标系平移回原来位置,使用平移 -T
具体公式如下:
M 为原始矩阵,是一个单位矩阵, M‘ 为结果矩阵, T 为平移, R为旋转
M' = M*T*R*-T = T*R*-T
按照公式写出来的伪代码如下:
Matrix matrix = new Matrix();
matrix.preTranslate(pivotX,pivotY);
matrix.preRotate(angle);
matrix.preTranslate(-pivotX, -pivotY);
围绕某一点操作可以拓展为通用情况,即:
Matrix matrix = new Matrix();
matrix.preTranslate(pivotX,pivotY);
// 各种操作,旋转,缩放,错切等,可以执行多次。
matrix.preTranslate(-pivotX, -pivotY);
公式为:
M' = M*T* ... *-T = T* ... *-T
但是这种方式,两个调整中心的平移函数就拉的太开了,所以通常采用这种写法:
Matrix matrix = new Matrix();
// 各种操作,旋转,缩放,错切等,可以执行多次。
matrix.postTranslate(pivotX,pivotY);
matrix.preTranslate(-pivotX, -pivotY);
这样公式为:
M' = T*M* ... *-T = T* ... *-T
但是这种方式,两个调整中心的平移函数就拉的太开了,所以通常采用这种写法:
Matrix matrix = new Matrix();
// 各种操作,旋转,缩放,错切等,可以执行多次。
matrix.postTranslate(pivotX,pivotY);
matrix.preTranslate(-pivotX, -pivotY);
这样公式为:
M' = T*M* ... *-T = T* ... *-T
可以看到最终化简结果是相同的。
在构造 Matrix 时,个人建议尽量使用一种乘法,前乘或者后乘,这样操作顺序容易确定,出现问题也比较容易排查。当然,由于矩阵乘法不满足交换律,前乘和后乘的结果是不同的,使用时应结合具体情景分析使用。
下面我们用不同对方式来构造一个相同的矩阵:
注意:
1.由于矩阵乘法不满足交换律,请保证使用初始矩阵(Initial Matrix),否则可能导致运算结果不同。
2.注意构造顺序,顺序是会影响结果的。
3.Initial Matrix是指new出来的新矩阵,或者reset后的矩阵,是一个单位矩阵。
1.仅用pre:
// 使用pre, M' = M*T*S = T*S
Matrix m = new Matrix();
m.reset();
m.preTranslate(tx, ty);
m.preScale(sx, sy);
用矩阵表示:
2.仅用post:
// 使用post, M‘ = T*S*M = T*S
Matrix m = new Matrix();
m.reset();
m.postScale(sx, sy); //,越靠前越先执行。
m.postTranslate(tx, ty);
3.混合:
// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
m.preScale(sx, sy);
m.postTranslate(tx, ty);
或:
// 混合 M‘ = T*M*S = T*S
Matrix m = new Matrix();
m.reset();
m.postTranslate(tx, ty);
m.preScale(sx, sy);
由于此处只有两步操作,且指定了先后,所以代码上交换并不会影响结果。
注意: 由于矩阵乘法不满足交换律,请保证初始矩阵为单位矩阵,如果初始矩阵不为单位矩阵,则导致运算结果不同。
上面虽然用了很多不同的写法,但最终的化简公式是一样的,这些不同的写法,都是根据同一个公式反向推算出来的。