前言
webgl走向3d的2d基础,偏移、缩放、旋转、复合矩阵犹如css的taranform的translate、scale、rotate,通过学习webgl的2d动态效果,那么你对2维的底层图形原理会有更深的认识。
二维移动demo
下面展示的是webgl的二维移动,看完基础知识后,相信你也能手敲一个
向量和矩阵的区别
向量可以理解为一个有方向有大小的直线段,例如vec3等,向量的算法一般有加法、减法、点积、叉积,在 lookat方法中可以一般会用到叉积和减法来实现相机环绕某个目标,而矩阵可以理解为处理向量的多维数组,例如mat3等,假如3 * 3的矩阵, 矩阵的计算我们一般以列,左侧为x,对每行第一项做乘法和,中间为y,对每行第二项做乘法和,最后为z,对每行第二项做乘法和。注意这里是列的计算。当然你也可以计算w, 有了矩阵我们可以简化很多我们之前的操作。
[1, 0, 0, X: 1*x + 0*y + 0*z
0, 1, 0, * vec3(x, y, z) -> Y: 0*x + 1*y + 0*z
0, 0, 1] Z: 0*x + 0*y + 1*z
线性代数
线性代数我们可以简单理解为通过输入某个线段,矩阵来处理,然后输出某个线段。线段对应向量,矩阵也就是mat,通过矩阵我们可以简化原来的屏幕坐标转换为裁剪坐标,只要传递一个mat就可以。包括偏移、缩放等。
矩阵
矩阵的意义
为什么需要矩阵?如果你之前不用矩阵,那么会有大量的attribute、unifrom的变量需要赋值,繁琐且不说,我通过一个矩阵就可以自由的计算,更加直观并容易组装和维护。矩阵的结构说白了就是数组,通过线性代数的计算方式来转换向量。
偏移矩阵、缩放矩阵、旋转矩阵
var m3 = {
translation: function (tx, ty) {
// x2 = x + tx y2 = y + ty z2 = 1
return new Float32Array([
1, 0, 0,
0, 1, 0,
tx, ty, 1,
]);
},
rotation: function (angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return new Float32Array([
c, -s, 0,
s, c, 0,
0, 0, 1,
]);
},
scaling: function (sx, sy) {
return new Float32Array([
sx, 0, 0,
0, sy, 0,
0, 0, 1,
]);
},
}
注意由于glsl是强类型语言,我们一般尽量定义类型约束,如Float32Array的存储32位浮点数数组,他的原型对象上同样存在与Array一样的方法。
我们先来看看偏移矩阵, 如果你理解了之前讲的矩阵的算法,那么你就能推导出这个公式
// x = 1 * x + 0 * y + tx * 1 ....
newX = x + tx;
newY = y + ty;
三角函数
正弦是对比斜,直角边与斜边的比值,余弦是临比斜,直角边临边与斜边的比值,sin正弦值对应为x坐标,cos余弦值对应y坐标。
//...
rotation: function (angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return new Float32Array([
c, -s, 0,
s, c, 0,
0, 0, 1,
]);
},
我们来看下三角函数在这里的具体应用,一般在canvas等的一些游戏场景,敌人的移动轨迹我们往往需要用正弦或余弦,通过一系列的计算那么可以模拟出随机的移动方式。
我们之前讲过webgl渲染到gpu是裁剪坐标,-1到1的范围,对应到半径为1的圆,可以得到4个切点,[0, 1]、[1, 0]、[0, -1]、[-1, 0],这里有个数学公式,针对不同图形的旋转得到旋转值,假设半径为1的圆,我们图形由于在屏幕坐标尺寸绝非是1半径,那么对应的旋转点坐标与基于半径为1的圆的旋转值我们可以得到屏幕坐标系的旋转后的点坐标, 那么我们可以通过下面的公式得到对应的旋转值,通过更新旋转值可以实现旋转。
rotatedX = a_position.x * u_rotation.y + a_position.y * u_rotation.x;
rotatedY = a_position.y * u_rotation.y - a_position.x * u_rotation.x;
function printSineAndCosineForAnAngle(angleInDegrees) {
var angleInRadians = angleInDegrees * Math.PI / 180;
var s = Math.sin(angleInRadians);
var c = Math.cos(angleInRadians);
console.log("s = " + s + " c = " + c);
}
function updateAngle (event, ui) {
var angleInDegrees = 360 - ui.value;
angleInRadians = angleInDegrees * Math.PI / 180;
rotation[0] = Math.sin(angleInRadians);
rotation[1] = Math.cos(angleInRadians);
drawScene();
}
但是一般我们会通过这种方法,通过传入角度,得到对应的弧长,传入正弦余弦得到最终的坐标。通过矩阵就可以旋转了。
单位矩阵
var m3 = {
// 单位矩阵
identity: function () {
return new Float32Array([
1, 0, 0,
0, 1, 0,
0, 0, 1,
])
},
}
单位矩阵其实就是矩阵的对角线都为1,其他位置值都为0,那么你输入的向量和输出的向量的xyz的值是一致的,一般我们用来定义初始值。
投影矩阵
var m3 = {
projection: function (width, height) {
// 注意:这个矩阵翻转了 Y 轴,所以 0 在上方
return new Float32Array([
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
]);
},
}
vec2 zeroToOne = a_position.xy / u_resolution;
vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
其实就是通过矩阵来简化我们原来的坐标转换,而不用像下面写4行代码,我们一行就可以搞定, 只要在js中修改对应的矩阵u_matrix。
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
矩阵相乘
function multiply (a, b) {
var a00 = a[0 * 3 + 0];
var a01 = a[0 * 3 + 1];
var a02 = a[0 * 3 + 2];
var a10 = a[1 * 3 + 0];
var a11 = a[1 * 3 + 1];
var a12 = a[1 * 3 + 2];
var a20 = a[2 * 3 + 0];
var a21 = a[2 * 3 + 1];
var a22 = a[2 * 3 + 2];
var b00 = b[0 * 3 + 0];
var b01 = b[0 * 3 + 1];
var b02 = b[0 * 3 + 2];
var b10 = b[1 * 3 + 0];
var b11 = b[1 * 3 + 1];
var b12 = b[1 * 3 + 2];
var b20 = b[2 * 3 + 0];
var b21 = b[2 * 3 + 1];
var b22 = b[2 * 3 + 2];
return [
b00 * a00 + b01 * a10 + b02 * a20,
b00 * a01 + b01 * a11 + b02 * a21,
b00 * a02 + b01 * a12 + b02 * a22,
b10 * a00 + b11 * a10 + b12 * a20,
b10 * a01 + b11 * a11 + b12 * a21,
b10 * a02 + b11 * a12 + b12 * a22,
b20 * a00 + b21 * a10 + b22 * a20,
b20 * a01 + b21 * a11 + b22 * a21,
b20 * a02 + b21 * a12 + b22 * a22,
];
},
当输入是两个矩阵相乘时,我们需要用第一个矩阵的行按顺序去乘以第二个矩阵的列,上面这种方法简单易读,除了用上面这种方法,我们也可以直接用循环实现简洁的代码:
function multiply(a, b) {
var result = [];
for (var i = 0; i < 3; i++) {
result[i] = [];
for (var j = 0; j < 3; j++) {
var sum = 0;
for (var k = 0; k < 3; k++) {
sum += a[i][k] * b[k][j];
}
result[i][j] = sum;
}
}
return result;
}
// 测试
var matrixA = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
var matrixB = [[9, 8, 7], [6, 5, 4], [3, 2, 1]];
var resultMatrix = multiply(matrixA, matrixB);
console.log(resultMatrix);
同时为了更低的时间复杂度,我们可以考虑用 Strassen算法实现,由于实现复杂本文不做展示。
复合矩阵
// 计算矩阵
var translationMatrix = m3.translation(translation[0], translation[1]);
var rotationMatrix = m3.rotation(angleInRadians);
var scaleMatrix = m3.scaling(scale[0], scale[1]);
// 通过矩阵设置视口
var projectionMatrix = m3.projection(
gl.canvas.clientWidth, gl.canvas.clientHeight);
// 改变旋转中心点
var moveOriginMatrix = m3.translation(-50, -75);
// 简化写法, 注意这里clientWidth是实际大小,防止width是百分比缩放的
matrix = m3.translate(matrix, translation[0], translation[1]);
matrix = m3.rotate(matrix, angleInRadians);
matrix = m3.scale(matrix, scale[0], scale[1]);
gl.uniformMatrix3fv(matrixLocation, false, matrix);
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
gl.uniform4fv(colorLocation, color);
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 18;
gl.drawArrays(primitiveType, offset, count);
之前我们去写偏移、渲染,你需要不断的加减乘除向量,那么我们通过矩阵可以更加直观简洁的做向量的转换。尤其是多个操作复合的情况下我们可以快速计算,并简化我们的代码。
其他矩阵
除了上述的矩阵,另外还有透视矩阵、逆矩阵等,他们的目的都是方便我们在图形上计算点的坐标,后续三维方面会做讲解。
图形绘制
// 核心点坐标的绘制
function setGeometry (gl) {
var width = 100;
var height = 150;
var thickness = 30;
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
// 顺时针绘制
0, 0,
30, 0,
0, 150,
0, 150,
30, 0,
30, 150,
30, 0,
100, 0,
30, 30,
30, 30,
100, 0,
100, 30,
30, 120,
100, 120,
30, 150,
30, 150,
100, 120,
100, 150
]),
gl.STATIC_DRAW);
}
gl.STATIC_DRAW代表可以多次绘制,这里定义了3个矩形,每个矩形由两个三角形组成,注意点的绘制方向为顺时针,同样也是bufferData缓冲区绘制。
总结
我们知道threejs是对webgl的封装,但是通过webgl我们可以看到很多图形学的原理和算法需要我们掌握,一般而言实际开发中我们会做大量的封装,包括多维体、类型、界面、矩阵、纹理、光照等,但是知道了这些webgl的实现与原理,threejs等三维框架,不管是opengl,我们都能快速上手,或者自己手敲一个简易版的threejs,也并不是一件难事。包括我们可以用webgl实现游戏引擎、3d图表库、建筑等三维领域等。
如果你对上文的内容不是很熟悉,可以看下我之前的文章,相信会对你有所帮助!
附录
本文正在参加「金石计划」