变换矩阵
本文将介绍3D渲染中的一个重要概念变换矩阵,下面是代码演示。
在介绍本文的代码之前,先要了解一个概念:矩阵。学过线性代数的朋友应该都知道矩阵相当于是一个二维数组,有自己的运算规则。下面就通过几个例子简单了解一下矩阵的特性。
3X3矩阵的加法 从图中可以看出3X3矩阵就像是一个3X3的表格,每个单元格中填写一个数。它的加法就是把两个矩阵对应位置的元素加起来放在结果矩阵对应的位置上。那么如果相加的两个矩阵尺寸不一样怎么办?答案是无法运算。矩阵的加减要求两边的矩阵必须尺寸相等。
下面是减法的运算,和加法相似,很好理解。
接下来是乘法。
乘法稍微有些复杂,所以我用符号来代替数值,方便观察规律。我们看结果的第一行第一列aj + bm + cp
,它就是左边的矩阵第一行和右边的矩阵第一列逐个相乘再相加的结果。
再看结果的第二行第一列aj + bm + cp
,它就是左边的矩阵第二行和右边的矩阵第一列逐个相乘再相加的结果。
差不多已经可以总结出规律了,结果矩阵的第n行第m列的结果就是左边的矩阵第n行和右边的矩阵第m列逐个相乘再相加的结果。读者可以看看其他的值是不是这样计算出来的。
最后说一下除法,矩阵的除法有些特殊,比如说B/A,可以换算成B*inv(A)。inv(A)是A的逆矩阵,因为不是所有矩阵都有逆矩阵,所以除法在矩阵计算中并不总是可用。逆矩阵的求解比较复杂,这里暂时就不解释了。本文目前也没有用到求逆矩阵的地方。如果读者感兴趣的话,可以自行百度或者翻一翻以前的线性代数课本。
变换矩阵
说完了矩阵,那么什么是变换矩阵呢?在图形绘制过程中,有三种变换,分别是平移,缩放,旋转。如果我们想要用代码表示一个3D环境中的变换需要几个变量呢,首先要有平移tx, ty, tz
,然后是缩放sx, sy, sz
,最后是旋转rx, ry, rz
。在渲染的时候把这些变量附加到原始的位置数据上就可以实现变换了。这种方式虽然可行但不够好,尤其是在GPU上这种方式产生的运算负担远大于使用矩阵。
attribute vec4 position;
varying vec4 fragColor;
uniform float elapsedTime;
uniform mat4 transform;
void main() {
fragColor = position * 0.5 + 0.5;
gl_Position = transform * position;
gl_PointSize = 20.0;
}
这是本文代码例子中的Vertex Shader,新增了一个uniform
uniform mat4 transform;
,mat4
这个类型前文有提到过,是4X4的矩阵。它是Shader内置的类型,支持直接加减乘等操作。使用矩阵会产生更少的运算指令,GPU可以更好的优化运算过程。那么应该怎么使用呢?接下来我就一一介绍每一种变换矩阵。
矩阵处理库
本文的例子使用的处理矩阵的库是glmatrix,我们可以使用它快速的构造各种我们需要的矩阵。
平移矩阵
假设有一个点(1, 2, 3)
,经过大小为(1, 2, 3)
的平移,最终必定会平移到(1+1, 2+2, 3+3)
的位置。使用矩阵计算如下。
这里补充一点,如果左边的矩阵的列数等于右边的矩阵的行数,它们就可以相乘,结果矩阵的行数等于左边矩阵的行数,列数等于右边矩阵的列数。
平移矩阵就是一个4X4的单位矩阵的第4行的前三个元素用tx,ty,tz填充之后的矩阵。下面就是一个单位矩阵。
下面是glmatrix中使用平移矩阵的用法。
translateMatrix = mat4.create();//创建单位矩阵
mat4.translate(translateMatrix, translateMatrix, vec3.fromValues(tx,ty,tz));//平移之后在赋值给translateMatrix
translate
的第一个参数是输出矩阵,第二个要要处理的矩阵,create
出来的是标准变换矩阵,经过translate
之后,就是平移矩阵了。
缩放矩阵
缩放矩阵的三个缩放元素sx,sy,sz,分布在从左到右的对角线上,矩阵相乘后位置的x,y,z分别乘以了sx,sy,sz,从而实现了缩放。
代码实现如下。
scaleMatrix = mat4.create();//创建单位矩阵
mat4.scale(scaleMatrix, scaleMatrix, vec3.fromValues(sx,sy,sz));//缩放之后在赋值给scaleMatrix
旋转矩阵
旋转矩阵相比于上面两个略微有些复杂,旋转包含两个重要元素,旋转的角度,绕什么轴旋转。具体原理可以参考三维空间中的旋转:旋转矩阵、欧拉角。代码实现如下。
rotateMatrix = mat4.create();//创建单位矩阵
mat4.rotate(rotateMatrix, rotateMatrix, Math.PI/2, vec3.fromValues(0,0,1.0));//缩放之后在赋值给rotateMatrix
Math.PI/2
是弧度,0.0,0.0,1.0
是旋转轴的向量。
综合三个矩阵
现在我们得到了三个矩阵,接下来就是把它们相乘,相乘之后的结果将同时具有这三种变换的能力。
var transform = mat4.create();
mat4.multiply(transform, rotateMatrix, scaleMatrix);
mat4.multiply(transform, transform, translateMatrix);
注意相乘的顺序translateMatrix * rotateMatrix * scaleMatrix
,这样可以保证先缩放再旋转,最后再平移。因为缩放和旋转的中心点是0,0,0点,如果先平移再缩放,每个顶点的位置已经改变,缩放和旋转出来的结果自然就不对了。
代码实现
最后回到本文的代码实现中来,我把之前的代码整理了一下,公用的东西移到了glBase.js文件里,在演示代码的资源Tab中可以找到它。glMatrix的js文件也可以在其中找到。
在glBase.js中,会调用window上的两个回调。一个是WebGL配置完成时的回调。
function setupGLEnv(canvasID) {
canvas = document.getElementById(canvasID);
gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
alert('天呐!您的浏览器竟然不支持WebGL,快去更新浏览器吧。');
}
program = makeProgram();
if (window.onWebGLLoad) {
window.onWebGLLoad();
}
}
另一个是每一次渲染时的回调。
function renderLoop() {
...
if (window.onWebGLRender) {
window.onWebGLRender(deltaTime, elapsedTime);
}
collectedFrameDuration += deltaTime;
collectedFrameCount++;
...
}
下面是本文的例子所有的代码。
var triangleBuffer = null;
function makeBuffer() {
var triangle = [
0.0, 0.5, 0.0, -0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
];
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(triangle), gl.STATIC_DRAW);
return buffer;
}
window.onWebGLLoad = function () {
triangleBuffer = makeBuffer();
}
window.onWebGLRender = function render(deltaTime, elapesdTime) {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(1.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, triangleBuffer);
positionLoc = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 4 * 3, 0);
elapsedTimeUniformLoc = gl.getUniformLocation(program, 'elapsedTime');
gl.uniform1f(elapsedTimeUniformLoc, elapesdTime);
var rotateMatrix = mat4.create();
mat4.rotate(rotateMatrix, rotateMatrix, elapesdTime / 1000.0, vec3.fromValues(1, 1, 1));
var scale = 0.1 * (Math.sin(elapesdTime / 1000.0) + 1.0) + 0.4;
var scaleMatrix = mat4.create();
mat4.scale(scaleMatrix, scaleMatrix, vec3.fromValues(scale, scale, scale));
var xoffset = Math.sin(elapesdTime / 1000.0);
var translateMatrix = mat4.create();
mat4.translate(translateMatrix, translateMatrix, vec3.fromValues(xoffset, 0, 0));
var transform = mat4.create();
mat4.multiply(transform, rotateMatrix, scaleMatrix);
mat4.multiply(transform, transform, translateMatrix);
transformUniformLoc = gl.getUniformLocation(program, 'transform');
gl.uniformMatrix4fv(transformUniformLoc, false, transform);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
我使用了那两个回调配置我的相关代码。新增的代码逻辑仅有一处。
var rotateMatrix = mat4.create();
mat4.rotate(rotateMatrix, rotateMatrix, elapesdTime / 1000.0, vec3.fromValues(1, 1, 1));
var scale = 0.1 * (Math.sin(elapesdTime / 1000.0) + 1.0) + 0.4;
var scaleMatrix = mat4.create();
mat4.scale(scaleMatrix, scaleMatrix, vec3.fromValues(scale, scale, scale));
var xoffset = Math.sin(elapesdTime / 1000.0);
var translateMatrix = mat4.create();
mat4.translate(translateMatrix, translateMatrix, vec3.fromValues(xoffset, 0, 0));
var transform = mat4.create();
mat4.multiply(transform, rotateMatrix, scaleMatrix);
mat4.multiply(transform, transform, translateMatrix);
transformUniformLoc = gl.getUniformLocation(program, 'transform');
gl.uniformMatrix4fv(transformUniformLoc, false, transform);
根据当前的时间配置了三个矩阵,相乘后赋值给uniform transform
。transform
是mat4
类型的,所以我使用了uniformMatrix4fv
来赋值,第一个参数是uniform的位置,第二个是指是否需要将矩阵进行转置操作,不需要就选择false
,第三个就是矩阵本身。
Vertex Shader
attribute vec4 position;
varying vec4 fragColor;
uniform float elapsedTime;
uniform mat4 transform;
void main() {
fragColor = position * 0.5 + 0.5;
gl_Position = transform * position;
gl_PointSize = 20.0;
}
Vertex Shader中增加了上面说的uniform transform
,并且在赋值给gl_Position
之前使用transform
将position
进行变换。
本篇主要介绍了什么是变换矩阵,如何使用变换矩阵以及怎样和Vertex Shader配合。下一篇就要开始介绍3D渲染最基础的概念之一,透视投影矩阵。