矩阵变量
GLSL ES语言中,通过关键字mat2、mat3、mat4声明矩阵,并提供内置函数mat2()、mat3()、mat4()构造矩阵。
支持三种方式初始化矩阵:
- 从第一列开始,逐列指定元素的值。这里与,高数中习惯按照逐行书写矩阵不同。
- 只传入一个值,作为对角矩阵的对角线值;
- 传入多个向量;
如下代码初始化2x2单位矩阵:
// 按照列优先,逐列指定每个元素
mat2 aMat = mat2(
1.0, 0, // 第一列
0, 1.0 // 第二列
);
// 构造对角矩阵,值是对角线的值
mat bMat = mat2(1.0);
vec2 aVec = vec2(1.0, 0);
vec2 bVec = vec2(0, 1.0);
// 使用向量构建矩阵
mat cMat = mat2(aVec, bVec);
可以向二维数组一样,使用索引下标访问元素。
mat2 aMat = mat2(
1.0, 0, // 第一列
0, 1.0 // 第二列
);
vec2 aVec = mat2[0]; // 访问第一列的向量,[1.0, 0]
vec2 bVec = mat2[0][1]; // 访问第一列第二行,0
mat2[1] = vec2(1.0, 0); // 给第二列重新赋值
矩阵运算
矩阵与数字加减
矩阵与数字的加减,也就是矩阵的每个元素与数字进行加减。示例如下:
mat2 aMat = mat2(
1.0, 0, // 第一列
0, 1.0 // 第二列
);
// 矩阵aMat加1之后, 得到新的矩阵
// [
// 2.0, 1, // 第一列
// 1, 2.0 // 第二列
// ]
//
mat2 bMat = aMat + 1;
矩阵与数字相乘
与矩阵与数字加减类似,矩阵与数字相乘,就是矩阵每个元素与数字相乘。示例如下:
mat2 aMat = mat2(
1.0, 0, // 第一列
0, 1.0 // 第二列
);
// 矩阵aMat乘以2之后, 得到新的矩阵
// [
// 2.0, 0, // 第一列
// 0, 2.0 // 第二列
// ]
//
mat2 bMat = aMat*2;
矩阵与矩阵相乘
回顾下线性代数关于矩阵相乘的知识。假设有A、B两个矩阵,有如下定义:
- 相乘的前提条件是,A矩阵的列数需等于B矩阵的行数;
- 矩阵相乘不遵守交换律,也就是说A⋅B ≠ B⋅A;
- 矩阵相乘遵守结合律,A.B.C = A.(B.C);
- A⋅B 也是一个矩阵,乘积矩阵的行数等于A矩阵,结果矩阵的列数等于B矩阵;
- 乘积矩阵的第n行,第m列的结果等于,A矩阵的第n行与B矩阵的第m列对应值的乘积之和。
具象地举例,A矩阵是3行3列,B矩阵是3行3列,乘积矩阵的第2行第2列的计算过程如下:
c5的计算结果 c5 = a4b2 + a5b5 + a6*b8
下面用GLSL表达如上矩阵相乘:
mat2 aMat = mat2(
a1, a4, a7,
a2, a5, a8,
a3, a6, a9
);
mat2 bMat = mat2(
b1, b4, b7,
b2, b5, b8,
b3, b6, b9,
);
// cMat = mat2(
// c1, c4, c7,
// c2, c5, c8,
// c3, c6, c9
// );
mat2 cMat = aMat * bMat;
空间变换
向量可以用来表示坐标、颜色等信息。WebGL可以用过vec2、vec3、vec4关键字声明向量。
vec2 aVec2 = vec2(1.0, 1.0);
vec3 aVec3 = vec3(1.0, 1.0, 1.0);
vec4 aVec4 = vec4(1.0, 1.0, 1.0, 1.0);
矩阵可以与向量相乘,计算过程和矩阵与矩阵相乘是一致的,因为向量可以看成多行单列矩阵。下图演示
3x3的矩阵与具有三个分量的向量相乘,结果也是一个向量。
下面用GLSL表达矩阵与向量相乘:
mat2 aMat = mat2(
a1, a4, a7,
a2, a5, a8,
a3, a6, a9
);
vec3 aVec = vec3(b1, b4, b7);
// cMat = mat2(
// c1, c4, c7,
// c2, c5, c8,
// c3, c6, c9
// );
mat2 cMat = aMat * bMat;
矩阵和向量相乘可以表达几何变换过程。矩阵与向量相乘,除了可以表示平移、缩放、旋转这三种基本几何变换,还可以表示组合变换。
平移
点p坐标是(x,y),经过x方向移动tx,y方向移动ty之后,新的坐标p1是(x + tx,y + ty)。平移矩阵是:
matrix = [
1, 0, 0, // 第一列
0, 1, 0, // 第二列
tx, ty, 1 // 第三列
]
为了满足矩阵相乘的条件,给原坐标p额外添加一个分量,变成(x,y,1)。矩阵相乘的过程如下图:
缩放
点p坐标是(x,y),经过x方向缩放sx,y方向缩放sy之后,新的坐标p1是(x * sx,y * sy)。缩放矩阵是:
matrix = [
sx, 0, 0, // 第一列
0, sy, 0, // 第二列
0, 0, 1 // 第三列
]
缩放变换过程,可以使用矩阵相乘进行如下表示:
旋转
点p坐标是(x,y),旋转弧度a之后,新的坐标p1是(xcos(a) + ysin(a),x*-sin(a) + y*cos(a))。缩放矩阵是:
matrix = [
cos(a), -sin(a), 0,
sin(a), cos(a), 0,
0, 0, 1
]
旋转变换过程,可以使用矩阵相乘进行如下表示:
组合
实际运用中,往往同时多种几何变换。假设坐标向量是D,平移矩阵是A、缩放矩阵是B、旋转矩阵C,那么坐标D经过平移、缩放、和旋转后的坐标向量如何表示了?
由于矩阵相乘遵循结合律,因此(C.(B.(A.D))) = (C.B.A).D,也就是,可以先把变换矩阵相乘,然后再与坐标向量相乘。这样的好处是,无论经过多少次变换,只需要向webgl传送一个组合矩阵,即使变换规则发生改变,着色器代码也无需变更。
如下代码,声明类Matrix3,提供了二维矩阵变换常用方法。我们先在js端计算组合矩阵combinationMatrix,然后通过uniformMatrix3fv传递给着色器代码。我们可以任意修改组合变换过程,着色器代码不需要做修改。
const vertextSource = `
attribute vec2 a_positon;
uniform mat3 u_matrix;
void main(void) {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
`;
class Matrix3 {
/**
* 列优先计算矩阵相乘
* @param left 左矩阵,3x3
* @param right 右矩阵,3x3
*/
static multiply(left: Array<number>, right: Array<number>) {
const combination = [];
for (let i = 0; i < 3; i++) { // 列
const rightColumn = right.slice(i * 3, i * 3 + 3);
for (let j = 0; j < 3; j++) { // 行
const leftColumn = [left[j], left[j + 3], left[j + 6]];
const result = leftColumn.reduce((sum, leftItem, index) => sum + leftItem * rightColumn[index], 0)
combination.push(result);
}
}
return combination;
}
static identity() {
return [
1, 0, 0,
0, 1, 0,
0, 0, 1,
];
}
static translation(matrix: Array<number>, tx: number, ty: number) {
return Matrix3.multiply(
matrix,
[
1, 0, 0,
0, 1, 0,
tx, ty, 1,
]
);
}
static rotation(matrix: Array<number>, angleInRadians: number) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return Matrix3.multiply(
matrix,
[
c,-s, 0,
s, c, 0,
0, 0, 1,
]
);
}
static scaling(matrix: Array<number>, sx: number, sy: number) {
return Matrix3.multiply(
matrix,
[
sx, 0, 0,
0, sy, 0,
0, 0, 1,
]
);
}
};
const rotatedMatrix = Matrix3.rotation(Matrix3.identity(), Math.PI / 2);
const scaledMatrix = Matrix3.scaling(rotatedMatrix, 2, 1);
const translatedMatrix =Matrix3.translation(scaledMatrix, 10, 10);
const uMatrix = gl.getUniformLocation(program, 'u_matrix');
gl.uniformMatrix3fv(uMatrix, false, translatedMatrix);
坐标系统变换
最后,我们举个实际的运用示例,感受下矩阵变换的意义。
在之前【零基础学WebGL】绘制图片,我们提到WebGL着色器代码的坐标系统是规范化设备坐标系,原点在画布中心,x轴方向从左到右,y轴方向从下向上,范围都是从-1到1。
但是,人类还是习惯使用屏幕坐标系,原点在画布左上角,x长度是画布宽度,y长度是画布高度 ,单位是px。
比如,我们想在宽300,高200的画布上绘制一个左上角坐标是(10,10),宽度是40的矩形。那么坐标改如何变换了?
首先,我们需求得屏幕系统转换到归一化的设备坐标系的变换矩阵。假设画布宽度width,高度height,变换过程如下:
屏幕坐标系统,经过x轴缩放1/width,y轴缩放1/height,范围变成0-1。用矩阵表示为:
[
1 / width, 0, 0,
0, 1 / height, 0,
0, 0, 1,
];
然后,将坐标系放大2倍,使得xy范围变成0-2。用矩阵表示为:
[
2, 0, 0,
0, 2, 0,
0, 0, 1,
];
然后,将x轴向反方向平移1个单位,y轴向反方向平移1个单位。用矩阵表示为:
[
1, 0, -1,
0, 1, -1,
0, 0, 1,
];
最后,对y轴进行翻转,实际上也是缩放变换。x轴不变,y轴缩放-1。用矩阵表示为:
[
1, 0, 0,
0, -1, 0,
0, 0, 1,
];
我们可以把上述变换过程,使用一个组合矩阵表达。
class Matrix3 {
...
static projection(canvasWidth: number, canvasHeight: number) {
const flipY = Matrix3.scale(Matrix3.identity(), 1, -1); // 翻转Y轴
const moveMinusOne = Matrix3.translate(flipY, -1, -1); // XY向反方向移动1个单位
const scaleDouble = Matrix3.scale(moveMinusOne, 2, 2); // XY放大两倍
const scaleCanvas = Matrix3.scale(scaleDouble, 1/ canvasWidth, 1/ canvasHeight); // 按照画布比例缩放
return scaleCanvas;
}
};
进一步,我们可以封装一个绘制矩形的方法。
const drawRectangle = (x: number, y: number, width: number, height: number) => {
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const postionData = [
x, y,
x + width, y,
x, y + height,
x, y + height,
x + width, y,
x + width, y + height
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(postionData), gl.STATIC_DRAW);
const aPosition = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
const uMatrix = gl.getUniformLocation(program, 'u_matrix');
const projectionMatrix = Matrix3.projection(gl.canvas.width, gl.canvas.height)
gl.uniformMatrix3fv(uMatrix, false, projectionMatrix);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}