矩阵
1. 矩阵的来源
英文matrix就是矩阵的意思,简单的说,矩阵用来改变顶点位置信息的,先牢记这句话,然后我们先从canvas2D入手,我们有一个100*100的canvas画布,然后画一个矩形:
<canvas width="100" height="100"></canvas>
ctx.rect(40, 40, 20, 20);
ctx.fill();
这样我们在画布中间画了一个矩形
现在我们希望将矩形向左移动10px:
ctx.rect(30, 40, 20, 20);
ctx.fill();

只要改变rect方法第一个参数就可以实现左右移动,因为rect()对应的就是一个矩形,是一个对象,canvas2D是对象级别的画布操作,但在3D的世界里,我们操作的是顶点:
<canvas id="c" width="200" height="200"></canvas>
var canvas = document.getElementById('c');
var webgl = canvas.getContext('webgl');
var vsScript = document.getElementById('shader-vs').innerText;
var fsScript = document.getElementById('shader-fs').innerText;
var vs = webgl.createShader(webgl.VERTEX_SHADER);
var fs = webgl.createShader(webgl.FRAGMENT_SHADER);
webgl.shaderSource(vs, vsScript);
webgl.shaderSource(fs, fsScript);
webgl.compileShader(vs);
webgl.compileShader(fs);
if(!webgl.getShaderParameter(vs, webgl.COMPILE_STATUS)) {
alert('error');
}
if(!webgl.getShaderParameter(fs, webgl.COMPILE_STATUS)) {
alert('error');
}
var program = webgl.createProgram();
webgl.attachShader(program, vs);
webgl.attachShader(program, fs);
webgl.linkProgram(program);
webgl.useProgram(program);
var aPosition = webgl.getAttribLocation(program, 'aPosition');
webgl.enableVertexAttribArray(aPosition);
var aPo = [
-0.2, -0.2, 0,
0.2, -0.2, 0,
0.2, 0.2, 0,
-0.2, 0.2, 0
];
var aIndex = [0, 1, 2, 0, 2, 3];
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
btn.onclick = function() {
var aPo = [
-0.3, -0.2, 0,
0.1, -0.2, 0,
0.1, 0.2, 0,
-0.3, 0.2, 0
];
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
btn.onclick = function() {};
};
同样也可以得到一个矩形。
这里我们可以看到position这个数组,这里面存的就是矩形4个点的顶点信息,我们可以通过操作改变其中点的值来改变位置,但是这样不累吗?有没有可以一次性改变某个物体所有顶点的方式呢?
答案是肯定的,那就是矩阵matrix。
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
以单位矩阵来说,我们用这个单位矩阵乘一个顶点(2,1,0):

这是我们看不到什么变化,那我们换一个矩阵来看:
1 0 0 1
0 1 0 0
0 0 1 0
0 0 0 1
再乘之前那个顶点:

再多用几个顶点试一下就会发现,无论我们用哪个顶点,都会得到这样的一个x坐标+1这样一个结果,这正好能解决我们前面提到的一次性改变顶点位置问题。
2. 矩阵规律
我们改变了矩阵16个值中的一个,就使得矩阵有改变顶点的能力,我们总结一下矩阵各个值的规律,如下图:
1 0 0 X
0 1 0 Y
0 0 1 Z
0 0 0 1
这里红色的x,y,z分别对应三个方向上的偏移:
X 0 0 0
0 Y 0 0
0 0 Z 0
0 0 0 1
这里蓝色的x,y,z分别对应三个方向上的缩放
然后就是围绕各个轴的旋转矩阵了:

还有剪切(skew)效果的变换矩阵,这里用个x轴的例子来体现:

这里都是某一种单一效果的变化矩阵,可以相乘配合使用,首先,似乎所有的操作都是围绕着红框这一块来的:

这里也比较好理解,因为矩阵每一行对应了个坐标:

那么问题是,最下面那行干啥用的?
一个顶点,坐标(x,y,z),这个是在笛卡尔坐标系中的表示,在3D世界中我们会将其转换为齐次坐标系,也就是变成了(x,y,z,w)。
那么齐次坐标有什么用呢?很多书上都说齐次坐标可以区分一个坐标是点还是向量,点的话齐次项是1,向量的话齐次项是0(所以之前图中w=1)。
也就是说对于3D世界中的Matrix,齐次项可以让物体有透视的效果
我们以透视矩阵为例:

在第四行的第三列有值,而不像之前的是0;还有第四行的第四列是0,而不是之前的1。这里涉及到正视和透视投影矩阵,这里不做深究,我们先只考虑红框部分的矩阵所带来的变化。
3. 3D坐标系
一般地,我们常用的矩阵有:
- MMatrix ---> 模型矩阵(用于物体在世界中变化)
- VMatrix ---> 视图矩阵(用于世界中摄像机的变化)
- PMatrix ---> 透视矩阵
模型矩阵和视图矩阵一般是仿射变换,也就是平移、旋转之类的变化,一个是先旋转,后平移(MMatrix),另一个是先平移,后旋转(VMatrix),给人的感觉,一个是物体本身在变化,一个是摄像机在变化。
那么对于PMatrix,我们先思考2D和3D的区别。它们在DOM中的宽高都是通过设置canvas标签上width和height属性来设置的,这很一致。但在3D中我们的坐标空间是-1 ~ 1:

(width=800,height=600中canvas2D中,矩形左顶点居中时,rect方法的前两个参数)

(width=800,height=600中3D中,矩形左顶点居中时,左顶点的坐标)
我们会发现x坐标小于-1或者大于1的的话就不会展示了(y同理),x和y很好理解,因为屏幕是2D的,画布是2D的,2D就只有x,y,也就是我们直观上所看到的东西,那z坐标靠什么来看到呢?
对比
首先至少有两个物体,它们的z坐标不同,这个z坐标会决定它们在屏幕上显示的位置(或者说覆盖)的情景:
var aPo = [
-0.2, -0.2, -0.5,
0.2, -0.2, -0.5,
0.2, 0.2, -0.5,
-0.2, 0.2, -0.5
];
var aIndex = [0, 1, 2, 0, 2, 3];
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.vertexAttrib3f(aColor, 1, 0, 0);
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
// 先画一个z轴是-0.5的矩形,颜色是红色
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
aPo = [
0, -0.4, -0.8,
0.4, -0.4, -0.8,
0.4, 0, -0.8,
0, 0, -0.8
];
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.vertexAttrib3f(aColor, 0, 1, 0);
// 再画一个z轴是-0.8的矩形,颜色是绿色
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
注意开启深度测试(不开启深度测试,计算机会无视顶点的z坐标信息,只关注drawElements(drawArrays)方法的调用顺序,最后画的一定是最上一层)
代码中A矩形(红色)的z值为-0.5,B矩形(绿色)的z值为-0.8,最终画布上谁会覆盖谁呢?
如果x=0.5和x=0.8之间,谁在左,谁在右,这个很好确定,因为屏幕就是2D的,画布坐标x轴就是右大左小。
“左手坐标系”和“右手坐标系”中x,y轴是一样的,如图所示:

而左手坐标系和右手坐标系中的z轴正方向不同,一个是屏幕向内,一个是屏幕向外,所以可以认为:如果左手坐标系下,B矩形(z=-0.8)小于A矩形(z=-0.5),那么理应覆盖了A矩形,右手坐标系恰恰相反:
var oC = document.getElementById('c');
var webgl = oC.getContext('webgl');
var vsScript = document.getElementById('shader-vs').innerText;
var fsScript = document.getElementById('shader-fs').innerText;
var vs = webgl.createShader(webgl.VERTEX_SHADER);
var fs = webgl.createShader(webgl.FRAGMENT_SHADER);
webgl.shaderSource(vs, vsScript);
webgl.shaderSource(fs, fsScript);
webgl.compileShader(vs);
webgl.compileShader(fs);
if(!webgl.getShaderParameter(vs, webgl.COMPILE_STATUS)) {
alert('error');
}
if(!webgl.getShaderParameter(fs, webgl.COMPILE_STATUS)) {
alert('error');
}
var program = webgl.createProgram();
webgl.attachShader(program, vs);
webgl.attachShader(program, fs);
webgl.linkProgram(program);
webgl.useProgram(program);
var aPosition = webgl.getAttribLocation(program, 'aPosition');
var aColor = webgl.getAttribLocation(program, 'aColor');
webgl.enableVertexAttribArray(aPosition);
webgl.enable(webgl.DEPTH_TEST);
var aPo = [
-0.2, -0.2, -0.5,
0.2, -0.2, -0.5,
0.2, 0.2, -0.5,
-0.2, 0.2, -0.5
];
var aIndex = [0, 1, 2, 0, 2, 3];
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.vertexAttrib3f(aColor, 1, 0, 0);
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
// 先画一个z轴是-0.1的矩形,颜色是红色
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
aPo = [
0, -0.4, -0.8,
0.4, -0.4, -0.8,
0.4, 0, -0.8,
0, 0, -0.8
];
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.vertexAttrib3f(aColor, 0, 1, 0);
// 再画一个z轴是-0.2的矩形,颜色是绿色
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
这里我们发现,这里的裁剪空间是按照左手坐标系来的。裁剪空间就是用来把超过坐标空间的东西切割掉(-1 ~ 1),其中裁剪空间的z坐标就是按照左手坐标系来的。
无论是PMatrix(透视投影矩阵)还是OMatrix(正视投影矩阵),它们都会操作裁剪空间,其中有一步就是将左手坐标系给转换为右手坐标系:
1 0 0 0
0 1 0 0
0 0 -1 0
0 0 0 1
我们只需要将z轴反转,就可以得到将裁剪空间由左手坐标系转变为右手坐标系了。
var oC = document.getElementById('c');
var webgl = oC.getContext('webgl');
var vsScript = document.getElementById('shader-vs').innerText;
var fsScript = document.getElementById('shader-fs').innerText;
var vs = webgl.createShader(webgl.VERTEX_SHADER);
var fs = webgl.createShader(webgl.FRAGMENT_SHADER);
webgl.shaderSource(vs, vsScript);
webgl.shaderSource(fs, fsScript);
webgl.compileShader(vs);
webgl.compileShader(fs);
if(!webgl.getShaderParameter(vs, webgl.COMPILE_STATUS)) {
alert('error');
}
if(!webgl.getShaderParameter(fs, webgl.COMPILE_STATUS)) {
alert('error');
}
var program = webgl.createProgram();
webgl.attachShader(program, vs);
webgl.attachShader(program, fs);
webgl.linkProgram(program);
webgl.useProgram(program);
var aPosition = webgl.getAttribLocation(program, 'aPosition');
var aColor = webgl.getAttribLocation(program, 'aColor');
webgl.enableVertexAttribArray(aPosition);
var uMatrix = webgl.getUniformLocation(program, 'uMatrix');
webgl.uniformMatrix4fv(uMatrix, false, [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1
])
webgl.enable(webgl.DEPTH_TEST);
var aPo = [
-0.2, -0.2, -0.5,
0.2, -0.2, -0.5,
0.2, 0.2, -0.5,
-0.2, 0.2, -0.5
];
var aIndex = [0, 1, 2, 0, 2, 3];
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.vertexAttrib3f(aColor, 1, 0, 0);
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
// 先画一个z轴是-0.1的矩形,颜色是红色
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
aPo = [
0, -0.4, -0.8,
0.4, -0.4, -0.8,
0.4, 0, -0.8,
0, 0, -0.8
];
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.vertexAttrib3f(aColor, 0, 1, 0);
// 再画一个z轴是-0.2的矩形,颜色是绿色
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
4. 结语
至于具体的PMatrix和OMatrix是怎么来的,Matrix能否进行一些优化,可以多做几个例子测试以下。
code enjoy! ٩(๑❛ᴗ❛๑)۶
作者:indeex
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。