1 前言

​ 本文主要介绍 MVP 矩阵变换,其本质是线性变换,应用见→绘制立方体

  • Model:模型变换,施加在模型上的空间变换,包含平移变换(translateM)、旋转变换(rotateM)、对称变换(transposeM)、缩放变换(scaleM);
  • View:观测变换,施加在观测点上的变换,用于调整观测点位置、观测朝向、观测正方向;
  • Projection:透视变换,施加在视觉上的变换,用于调整模型的透视效果(如:矩形的透视效果是梯形)。

​ 上述变换依次叠加,得到一个总的变换矩阵,即 MVP 变换矩阵,mvpMatrix = projectionMatrix * viewMatrix * modelMatrix,MVP 变换作用到模型的原始坐标矩阵上,得到的最终坐标矩阵即为用户观测到的模型状态。

​ 空间坐标系建立后,空间中任何一个点都对应一个向量,设空间中一向量为 [a, b, c]',为方便采用统一的格式描述线性变换,将三维空间中的向量 [a, b, c]' 映射到四维空间中的向量 [a, b, c, 1]',同时,线性变换采用 4x4 的矩阵描述。所有向量 [a, b, c, 1]' 生成的空间仍然是一个三维空间,它是四维空间中的一个三维子空间。

​ 对与某些线性变换(如平移变换),若使用 4x4 矩阵描述,此矩阵可以固定,不必依赖待变换的向量;若使用 3x3 矩阵描述,此矩阵不能固定,随着待变换的向量而变化。由此说明, 4x4 矩阵具有更强的描述性。

​ 设三维空间中的任意向量按照以上规则映射到四维空间中的向量为 v = [a, b, c, 1]',变换矩阵为 A 。OpenGL 为通用化接口,在获取变换矩阵时,会左乘一个初始矩阵 M,即将 MA 作为最终的变换矩阵,通常情况 M 为单位矩阵(E),即 M = E。(v 为列向量,A、M、E 都是 4x4 矩阵)

​ 说明:本文涉及的矩阵变换的方法都是 android.opengl.Matrix 类中的静态方法。

2 模型变换

​ 在介绍矩阵变换前,先介绍下单位矩阵,opengl 中初始化单位矩阵的方法如下:

public static void setIdentityM(float[] sm, int smOffset) {
	for (int i = 0 ; i < 16 ; i++) {
		sm[smOffset + i] = 0;
	}
	for(int i = 0; i < 16; i += 5) {
		sm[smOffset + i] = 1.0f;
	}
}

​ 说明:smOffset = sm.length - 16,sm 数组中 smOffset 之前的数,不参入接下来的线性变换。

2.1 平移变换

​ 设平移向量为 [x, y, z, 0]',则 A * v 如下:

img

​ 若用 3x3 矩阵描述平移变换,即 A 为 3x3 矩阵,则 A 中的元素必须依赖于 a, b, c,这样就会降低变换的一般性(或通用性),这也解释了为什么平移变换要用 4x4 矩阵描述的问题。

​ M * A 如下:

img

​ 源码如下,m 为初始矩阵,通常取单位矩阵;moffset 为索引偏移,表示 m 中 moffset 之前的数不参与变换,通常取 0;x , y , z 为向量 v 在 x轴、y 轴、z轴上的平移值。

public static void translateM(
		float[] m, int mOffset,
		float x, float y, float z) {
	for (int i = 0 ; i < 4 ; i++) {
		int mi = mOffset + i;
		m[12 + mi] += m[mi] * x + m[4 + mi] * y + m[8 + mi] * z;
	}
}

2.2 旋转变换

​ 本节仅列出沿着 x 轴、y 轴、z 轴旋转的变换矩阵,对于沿任意轴的旋转,公式比较复杂,本文不作讨论。设旋转角度为 θ(角度制),其对应的弧度为 α。

1)沿 x 轴旋转

img

2)沿 y 轴旋转

img

3)沿 z 轴旋转

img

​ 相似变换可以用 3x3 矩阵描述,保证 A 中的元素不依赖于 a, b, c,但是为了提高接口的通用性, 线性变换统一使用 4x4 矩阵描述。

​ 源码如下,rm 为旋转变换返回的矩阵;rmoffset 为索引偏移,表示 rm 中 rmoffset 之前的数不参与变换,通常取 0;a 为旋转角度(角度制);x , y , z 为旋转轴。旋转时,按照右手原则逆时针旋转 a 度。

public static void setRotateM(float[] rm, int rmOffset,
		float a, float x, float y, float z) {
	rm[rmOffset + 3] = 0;
	rm[rmOffset + 7] = 0;
	rm[rmOffset + 11]= 0;
	rm[rmOffset + 12]= 0;
	rm[rmOffset + 13]= 0;
	rm[rmOffset + 14]= 0;
	rm[rmOffset + 15]= 1;
	a *= (float) (Math.PI / 180.0f);
	float s = (float) Math.sin(a);
	float c = (float) Math.cos(a);
	if (1.0f == x && 0.0f == y && 0.0f == z) { //沿x轴旋转
		rm[rmOffset + 5] = c;   rm[rmOffset + 10]= c;
		rm[rmOffset + 6] = s;   rm[rmOffset + 9] = -s;
		rm[rmOffset + 1] = 0;   rm[rmOffset + 2] = 0;
		rm[rmOffset + 4] = 0;   rm[rmOffset + 8] = 0;
		rm[rmOffset + 0] = 1;
	} else if (0.0f == x && 1.0f == y && 0.0f == z) { //沿y轴旋转
		rm[rmOffset + 0] = c;   rm[rmOffset + 10]= c;
		rm[rmOffset + 8] = s;   rm[rmOffset + 2] = -s;
		rm[rmOffset + 1] = 0;   rm[rmOffset + 4] = 0;
		rm[rmOffset + 6] = 0;   rm[rmOffset + 9] = 0;
		rm[rmOffset + 5] = 1;
	} else if (0.0f == x && 0.0f == y && 1.0f == z) { //沿z轴旋转
		rm[rmOffset + 0] = c;   rm[rmOffset + 5] = c;
		rm[rmOffset + 1] = s;   rm[rmOffset + 4] = -s;
		rm[rmOffset + 2] = 0;   rm[rmOffset + 6] = 0;
		rm[rmOffset + 8] = 0;   rm[rmOffset + 9] = 0;
		rm[rmOffset + 10]= 1;
	} else { //沿任意轴旋转
		float len = length(x, y, z);
		if (1.0f != len) { //正则化
			float recipLen = 1.0f / len;
			x *= recipLen;
			y *= recipLen;
			z *= recipLen;
		}
		float nc = 1.0f - c;
		float xy = x * y;
		float yz = y * z;
		float zx = z * x;
		float xs = x * s;
		float ys = y * s;
		float zs = z * s;
		rm[rmOffset +  0] = x*x*nc +  c;
		rm[rmOffset +  4] =  xy*nc - zs;
		rm[rmOffset +  8] =  zx*nc + ys;
		rm[rmOffset +  1] =  xy*nc + zs;
		rm[rmOffset +  5] = y*y*nc +  c;
		rm[rmOffset +  9] =  yz*nc - xs;
		rm[rmOffset +  2] =  zx*nc - ys;
		rm[rmOffset +  6] =  yz*nc + xs;
		rm[rmOffset + 10] = z*z*nc +  c;
	}
}

2.3 对称变换

​ 设对称轴 u = [x, y, z, 1]',则 A * v + v = 2 * u,即

img

​ 若用 3x3 矩阵描述对称变换,即 A 为 3x3 矩阵,则 A 中的元素必须依赖于 a, b, c,这样就会降低变换的一般性(或通用性),这也解释了为什么对称变换要用 4x4 矩阵描述的问题。

​ M * A 如下:

img

​ 代码如下(Matrix 类中没有对称变换,此方法是笔者写的),m 为初始矩阵,通常取单位矩阵;moffset 为索引偏移,表示 m 中 moffset 之前的数不参与变换,通常取 0; [x, y, z, 1]' 为对称轴。

public static void symmetryM(float[] m, int mOffset,
		float x, float y, float z) {
	for (int i = 0 ; i < 12 ; i++) {
		int mi = mOffset + i;
		m[mi] = -m[mi];
	}
	for (int i = 0 ; i < 4 ; i++) {
		int mi = mOffset + i;
		m[12 + mi] += 2 * (m[mi] * x + m[4 + mi] * y + m[8 + mi] * z);
	}
}

2.4 缩放变换

​ 设向量 v 在 x 轴、y 轴、z 轴上的缩放分量分别为 x, y, z,则 A * v 如下:

img

​ 缩放变换可以用 3x3 矩阵描述,保证 A 中的元素不依赖于 a, b, c,但是为了提高接口的通用性, 线性变换统一使用 4x4 矩阵描述。

​ M * A 如下:

img

​ 源码如下,m 为初始矩阵,通常取单位矩阵;moffset 为索引偏移,表示 m 中 moffset 之前的数不参与变换,通常取 0; x, y, z 为向量 v 在 x 轴、y 轴、z 轴上的缩放分量。

public static void scaleM(float[] m, int mOffset,
		float x, float y, float z) {
	for (int i=0 ; i<4 ; i++) {
		int mi = mOffset + i;
		m[mi] *= x;
		m[4 + mi] *= y;
		m[8 + mi] *= z;
	}
}

3 观测变换

​ 设观察点坐标为 eye = [eyeX, eyeY, eyeZ, 1]',观察方向为 f = [fx, fy, fz, 0]',观察向上的正方向为 u = [ux, uy, uz, 0]',向量 s = [sx, sy, sz, 0]' 为 u x f 的方向向量(x 为向量叉乘)。说明:假设 f、u、s 都是单位向量(可以通过正则化做到),并且它们相互正交(若 f 与 u 不正交,可以通过 f x s 反向求出 u),向量 u 可以理解为相机的摆向,如:可以横着拍照,也可以竖着或斜着拍照。令 e = [0, 0, 0, 1]',则矩阵 A' = [s, u, -f, e] 为 正规矩阵,所以A'·A=E,A-1=A‘ 。

​ 观测变换的本质是:建立一个新的坐标系,该坐标系以 eye 为坐标原点,以 s, u, -f 为 x 轴、 y 轴、z 轴,将原向量 v 映射到新坐标系下的向量 u。

​ 设向量 e1 = [1, 0, 0, 0]',e2 = [0, 1, 0, 0]',e3 = [0, 0, 1, 0]',e4 = [0, 0, 0, 1]',则向量 s、u、-f、e 在向量 e1、e2、e3、e4 下的矩阵表示如下:

img

​ 由于 A' 是正规矩阵,即 A'·A=E,因此 向量 e1、e2、e3、e4 在向量 s、u、-f、e 下的矩阵表示如下:

img

​ 对于任意向量 v = [a, b, c, 1] 在向量 s、u、-f、e 下的矩阵表示如下:

img

​ 观测变换可以用 3x3 矩阵描述,保证 A 中的元素不依赖于 a, b, c,但是为了提高接口的通用性, 线性变换统一使用 4x4 矩阵描述。

​ 源码如下,rm 是观测变换返回的矩阵;rmoffset 为索引偏移,表示 rm 中 rmoffset 之前的数不参与变换,通常取 0;[eyeX, eyeY, eyeZ, 1] 为观测点处坐标(相机坐标);[centerX, centerY, centerZ, 1] 为观测焦点处坐标(决定相机往哪个方向拍),[upX, upY, upZ, 0] 为相机摆向向量(决定相机是横着、竖着还是斜着放)。

public static void setLookAtM(float[] rm, int rmOffset,
		float eyeX, float eyeY, float eyeZ,
		float centerX, float centerY, float centerZ, float upX, float upY,
		float upZ) {
    // 计算 f 向量
	float fx = centerX - eyeX;
	float fy = centerY - eyeY;
	float fz = centerZ - eyeZ;

	// 正则化 f
	float rlf = 1.0f / Matrix.length(fx, fy, fz);
	fx *= rlf;
	fy *= rlf;
	fz *= rlf;

	// 计算 s 向量,s = f x up (x 为向量叉乘运算)
	float sx = fy * upZ - fz * upY;
	float sy = fz * upX - fx * upZ;
	float sz = fx * upY - fy * upX;

	// 正则化 s
	float rls = 1.0f / Matrix.length(sx, sy, sz);
	sx *= rls;
	sy *= rls;
	sz *= rls;

	// 计算 u 向量,u = s x f
	float ux = sy * fz - sz * fy;
	float uy = sz * fx - sx * fz;
	float uz = sx * fy - sy * fx;

	rm[rmOffset + 0] = sx;
	rm[rmOffset + 1] = ux;
	rm[rmOffset + 2] = -fx;
	rm[rmOffset + 3] = 0.0f;

	rm[rmOffset + 4] = sy;
	rm[rmOffset + 5] = uy;
	rm[rmOffset + 6] = -fy;
	rm[rmOffset + 7] = 0.0f;

	rm[rmOffset + 8] = sz;
	rm[rmOffset + 9] = uz;
	rm[rmOffset + 10] = -fz;
	rm[rmOffset + 11] = 0.0f;

	rm[rmOffset + 12] = 0.0f;
	rm[rmOffset + 13] = 0.0f;
	rm[rmOffset + 14] = 0.0f;
	rm[rmOffset + 15] = 1.0f;
    //将坐标中心由 [0, 0, 0, 0] 平移到 -eye 处
	translateM(rm, rmOffset, -eyeX, -eyeY, -eyeZ);
}

​ 说明:源码中 u 向量未正则化,这个可以省略,其模长|u|=|s x f|=|u|·|f|·sinα= sinα=1。

4 透视变换

​ 关于透视变换的详细介绍→透视变换原理,本文仅给出 frustumM 方法和 perspectiveM 方法源码。

1)perspectiveM 方法

​ 源码如下,m 是透视变换返回的矩阵;offset 为索引偏移,表示 m 中 offset 之前的数不参与变换,通常取 0;fovy 为视野角度(角度制);aspect 为屏幕宽高比;zNear 为近平面到相机的距离;zFar 为远平面到相机的距离。

public static void perspectiveM(float[] m, int offset,
	  float fovy, float aspect, float zNear, float zFar) {
	float f = 1.0f / (float) Math.tan(fovy * (Math.PI / 360.0));
	float rangeReciprocal = 1.0f / (zNear - zFar);

	m[offset + 0] = f / aspect;
	m[offset + 1] = 0.0f;
	m[offset + 2] = 0.0f;
	m[offset + 3] = 0.0f;

	m[offset + 4] = 0.0f;
	m[offset + 5] = f;
	m[offset + 6] = 0.0f;
	m[offset + 7] = 0.0f;

	m[offset + 8] = 0.0f;
	m[offset + 9] = 0.0f;
	m[offset + 10] = (zFar + zNear) * rangeReciprocal;
	m[offset + 11] = -1.0f;

	m[offset + 12] = 0.0f;
	m[offset + 13] = 0.0f;
	m[offset + 14] = 2.0f * zFar * zNear * rangeReciprocal;
	m[offset + 15] = 0.0f;
}

2)frustumM 方法

​ 源码如下,m 是透视变换返回的矩阵;offset 为索引偏移,表示 m 中 offset 之前的数不参与变换,通常取 0;(left, right, bottom, top) 为投影平面的边框,通常取 (-ratio, ratio, -1, 1);near 为近平面到相机的距离;far 为远平面到相机的距离。

public static void frustumM(float[] m, int offset,
		float left, float right, float bottom, float top,
		float near, float far) {
    ... //输入合法性校验
	final float r_width  = 1.0f / (right - left);
	final float r_height = 1.0f / (top - bottom);
	final float r_depth  = 1.0f / (near - far);
	final float x = 2.0f * (near * r_width);
	final float y = 2.0f * (near * r_height);
	final float A = (right + left) * r_width;
	final float B = (top + bottom) * r_height;
	final float C = (far + near) * r_depth;
	final float D = 2.0f * (far * near * r_depth);
	m[offset + 0] = x;
	m[offset + 5] = y;
	m[offset + 8] = A;
	m[offset +  9] = B;
	m[offset + 10] = C;
	m[offset + 14] = D;
	m[offset + 11] = -1.0f;
	m[offset +  1] = 0.0f;
	m[offset +  2] = 0.0f;
	m[offset +  3] = 0.0f;
	m[offset +  4] = 0.0f;
	m[offset +  6] = 0.0f;
	m[offset +  7] = 0.0f;
	m[offset + 12] = 0.0f;
	m[offset + 13] = 0.0f;
	m[offset + 15] = 0.0f;
}

​ 声明:本文转自【OpenGL ES】MVP矩阵变换