矩阵

内容纲要

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坐标系

一般地,我们常用的矩阵有:

  1. MMatrix ---> 模型矩阵(用于物体在世界中变化)
  2. VMatrix ---> 视图矩阵(用于世界中摄像机的变化)
  3. 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

链接:http://indeex.cc

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


发表评论

您的电子邮箱地址不会被公开。