creator中矩阵究竟是如何参与坐标运算的

本地矩阵和世界矩阵

显示节点CCNode,内部有一个变量,叫_trs,注意这个很重要,它主要存储了以下几个数据,我们在外部修改节点下面这些数据的时候,数据会优先存储到这个数组里,然后标记矩阵为脏,表示变换矩阵需要更新,那么在渲染的时候,就会依据flag对矩阵进行更新,进而对节点进行更新,这部分数据,只有位置,旋转,缩放

/*
trs[0] = 0; // position.x
trs[1] = 0; // position.y
trs[2] = 0; // position.z
trs[3] = 0; // rotation.x
trs[4] = 0; // rotation.y
trs[5] = 0; // rotation.z
trs[6] = 1; // rotation.w
trs[7] = 1; // scale.x
trs[8] = 1; // scale.y
trs[9] = 1; // scale.z
*/

那这个变换矩阵到底是啥呢?
一个点的坐标是(x,y,z),可以将其看成一个向量,那么可以构造一个矩阵和它相乘,然后得出一个新的向量,现在要做的就是构造一个最原始的矩阵,就是与这个点相乘,这个点的坐标不会发生任何变化,那么很容易想到这个矩阵就是单位矩阵

在这里插入图片描述
可以动手试一下,用这个矩阵和一个点的向量相乘,点的向量是不会发生任何变化的,现在要做一个转变,就是要把对点的变换数据全部存储到这个矩阵中,而不是直接体现在当前这个点的数据上,这样的话,一个点一开始加到场景中会记录他的数据作为原始数据,之后对这个点所有的变换都放到了矩阵中,对于旋转和缩放,这个33的矩阵完全hold的住,可是平移变换就不行了,那么咋办呢,不要慌,只需要再加个维度,变成44的单位矩阵就ok啦
X Y Z W
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1

当渲染的时候或特别需要的时候,那么如果判断矩阵的flag为脏,就开始更新了,请看CCNode.js中是如何更新的

function updateLocalMatrix2D () {
    let dirtyFlag = this._localMatDirty;
    if (!(dirtyFlag & LocalDirtyFlag.TRSS)) return;

    // Update transform
    let t = this._matrix;
    let tm = t.m;
    //这是重点要记住,这里面存储了变换的数据
    let trs = this._trs;

    if (dirtyFlag & (LocalDirtyFlag.RS | LocalDirtyFlag.SKEW)) {
        let rotation = -this._eulerAngles.z;
        let hasSkew = this._skewX || this._skewY;
        let sx = trs[7], sy = trs[8];//缩放

        if (rotation || hasSkew) {
            let a = 1, b = 0, c = 0, d = 1;
            // rotation
            if (rotation) {
                let rotationRadians = rotation * ONE_DEGREE;
                c = Math.sin(rotationRadians);
                d = Math.cos(rotationRadians);
                a = d;
                b = -c;
            }
            // scale
            tm[0] = a *= sx;
            tm[1] = b *= sx;
            tm[4] = c *= sy;
            tm[5] = d *= sy;
            // skew
            if (hasSkew) {
                let a = tm[0], b = tm[1], c = tm[4], d = tm[5];
                let skx = Math.tan(this._skewX * ONE_DEGREE);
                let sky = Math.tan(this._skewY * ONE_DEGREE);
                if (skx === Infinity)
                    skx = 99999999;
                if (sky === Infinity)
                    sky = 99999999;
                tm[0] = a + c * sky;
                tm[1] = b + d * sky;
                tm[4] = c + a * skx;
                tm[5] = d + b * skx;
            }
        }
        else {
            tm[0] = sx;
            tm[1] = 0;
            tm[4] = 0;
            tm[5] = sy;
        }
    }

    // position
    tm[12] = trs[0];
    tm[13] = trs[1];
    
    this._localMatDirty &= ~LocalDirtyFlag.TRSS;
    // Register dirty status of world matrix so that it can be recalculated
    this._worldMatDirty = true;
}

新的矩阵更新完毕,要开始更新顶点了,每一种类型的节点,都会有一个assember,这个主要就是来更新顶点位置和上传数据到顶点buff和索引buffer的,就举个简单的例子,请看assembler-2d.js中是如何更新点的位置的

updateWorldVerts (comp) {
        let local = this._local;
        let verts = this._renderData.vDatas[0];

        let matrix = comp.node._worldMatrix;
        let matrixm = matrix.m,
            a = matrixm[0], b = matrixm[1], c = matrixm[4], d = matrixm[5],
            //x轴的偏移偏移    y轴偏移
            tx = matrixm[12], ty = matrixm[13];

        let vl = local[0], vr = local[2],
            vb = local[1], vt = local[3];
        
        let justTranslate = a === 1 && b === 0 && c === 0 && d === 1;

        if (justTranslate) {
            // left bottom
            verts[0] = vl + tx;
            verts[1] = vb + ty;
            // right bottom
            verts[5] = vr + tx;
            verts[6] = vb + ty;
            // left top
            verts[10] = vl + tx;
            verts[11] = vt + ty;
            // right top
            verts[15] = vr + tx;
            verts[16] = vt + ty;
        } else {
            let al = a * vl, ar = a * vr,
            bl = b * vl, br = b * vr,
            cb = c * vb, ct = c * vt,
            db = d * vb, dt = d * vt;

            // left bottom
            verts[0] = al + cb + tx;
            verts[1] = bl + db + ty;
            // right bottom
            verts[5] = ar + cb + tx;
            verts[6] = br + db + ty;
            // left top
            verts[10] = al + ct + tx;
            verts[11] = bl + dt + ty;
            // right top
            verts[15] = ar + ct + tx;
            verts[16] = br + dt + ty;
        }
    }

详细解说

canvas:舞台节点
scene:场景节点
Node:普通节点
第一步:
每一个节点都可看成一个坐标系,canvas的本地矩阵和世界矩阵是一样的
第二步
scene的本地矩阵,就是针对scene上的节点位置,以scene原点组成的矩阵Matrix(scene_m)
scene的世界矩阵,就是scene的位置组成的4x4的单位矩阵Matrix(scene_w);
现在要把场景节点的位置数据转换到世界空间中
p(w) = Matrix(scene_w) * matrix(scene_m) x p(m);
第三步
现在将scene上一级普通节点上的点转换到世界空间中;
Node的本地矩阵,就是针对Node上的节点位置,以Node原点组成的矩阵
Matrix(Node_m)
Node的世界矩阵,就是Node的位置组成的4x4的单位矩阵Matrix(node_w),
这里求得世界矩阵是基于他父亲的,并不是基于canvas
p(w) = Matrix(node_w) * matrix(node_m)*p(m);
这里求出来的点实际上是转换到了scene的空间坐标系下,如果想转换到基于canvas的世界坐标系,只需要乘以scene的世界矩阵即可
若干步
二级,三级,…n级普通节点,也是如此,如果想把二级节点的点转换到基于canvas的空间坐标系下,只需要一级一级往上转换即可

看代码CCNode.js

_calculWorldMatrix () {
        // Avoid as much function call as possible
        if (this._localMatDirty & LocalDirtyFlag.TRSS) {
            this._updateLocalMatrix();
        }
        // Assume parent world matrix is correct
        let parent = this._parent;
        if (parent) {
            //计算世界矩阵
            this._mulMat(this._worldMatrix, parent._worldMatrix, this._matrix);
        }
        else {
            Mat4.copy(this._worldMatrix, this._matrix);
        }
        this._worldMatDirty = false;
    }

每一个节点,都会去计算本地矩阵和世界矩阵,所以每个节点的世界矩阵都是一层一层的往上追溯计算得到的,节点与节点直接都是相互继承的
所以如果求一个节点的世界矩阵,就是拿当前节点的本地矩阵和它父节点世界矩阵,此时所得结果就是世界矩阵

矩阵顺序表
【0 4 8 12】
【1 5 9 13】
【2 6 10 14】
【3 7 11 15】
最原始的localMatrix,其实他就是(x,y,z,w)这四个列向量组成的方阵
x y z w
【1 0 0 0】
【0 1 0 0】
【0 0 1 0】
【0 0 0 1】
最原始的worldMatrix
【1 0 0 node.x】
【0 1 0 node.y】
【0 0 1 0】
【0 0 0 1】

一个场景中的显示节点,有它的位置,通过这个位置我们可以算出来它相对于父节点的矩阵,依照继承关系,一层一层往上算,算出来它的世界矩阵,每一个节点都持有本地矩阵和世界矩阵,那么节点一般都是有尺寸,比如显示一张2d图片,其实他就是一个矩形,这个时候需要算出来它的四个顶点即(左上角位置,左下角位置,右上角位置,右下角位置),其实对于画一个2d矩形,这四个顶点就是我们要找的,我们要把它传给GPU, 看creator中如何实现的

updateVerts (sprite) {
        let node = sprite.node,
            cw = node.width, ch = node.height,
            appx = node.anchorX * cw, appy = node.anchorY * ch,
            l, b, r, t;
        if (sprite.trim) {
            l = -appx;
            b = -appy;
            r = cw - appx;
            t = ch - appy;
        }
        else {
            let frame = sprite.spriteFrame,
                ow = frame._originalSize.width, oh = frame._originalSize.height,
                rw = frame._rect.width, rh = frame._rect.height,
                offset = frame._offset,
                scaleX = cw / ow, scaleY = ch / oh;
            let trimLeft = offset.x + (ow - rw) / 2;
            let trimRight = offset.x - (ow - rw) / 2;
            let trimBottom = offset.y + (oh - rh) / 2;
            let trimTop = offset.y - (oh - rh) / 2;
            l = trimLeft * scaleX - appx;
            b = trimBottom * scaleY - appy;
            r = cw + trimRight * scaleX - appx;
            t = ch + trimTop * scaleY - appy;
        }

        let local = this._local;
        local[0] = l;
        local[1] = b;
        local[2] = r;
        local[3] = t;
        this.updateWorldVerts(sprite);
    }

总结
本地矩阵主要处理的是记录节点的三个变换(旋转,缩放,平移)信息,他是一个4x4的方正矩阵
世界矩阵主要处理空间变换,是由当前节点的相对于其父节点的位置组成的4x4的方阵矩阵,通过继承关系,逐层计算,最终算出当前节点的世界矩阵,每个节点的世界矩阵都是有差异化的,矩阵就是一个记录节点数据变换的一个方阵,一般每个节点数据肯定是不同的

视口矩阵和投影矩阵

懂了上面的内容,那这个视口矩阵就太简单了,首先视口指的是摄像机,摄像机也是放在场景中的一个节点,上面说的都是从普通节点求他的世界矩阵,这个地方就是取反,就是说我们根据摄像机的节点位置,来求出摄像机节点的世界矩阵,那么视口矩阵就是它世界矩阵的逆矩阵。关于投影矩阵,这个是死的,用相机的参数创建而成功,这个没啥好说的看代码ccNode.js和CCCamera.js

//ccnode.js
//获取当前节点的世界矩阵
getWorldRT (out) {
        let opos = _gwrtVec3a;
        let orot = _gwrtQuata;
        let ltrs = this._trs;
        Trs.toPosition(opos, ltrs);
        Trs.toRotation(orot, ltrs);

        let curr = this._parent;
        while (curr) {
            ltrs = curr._trs;
            // opos = parent_lscale * lpos
            Trs.toScale(_gwrtVec3b, ltrs);
            Vec3.mul(opos, opos, _gwrtVec3b);
            // opos = parent_lrot * opos
            Trs.toRotation(_gwrtQuatb, ltrs);
            Vec3.transformQuat(opos, opos, _gwrtQuatb);
            // opos = opos + lpos
            Trs.toPosition(_gwrtVec3b, ltrs);
            Vec3.add(opos, opos, _gwrtVec3b);
            // orot = lrot * orot
            Quat.mul(orot, _gwrtQuatb, orot);
            curr = curr._parent;
        }
        Mat4.fromRT(out, orot, opos);
        return out;
    },
    //cccamra.js
 _calcMatrices (width, height) {
    // view matrix
    this._node.getWorldRT(_matViewInv);
    Mat4.invert(_matView, _matViewInv);

    // projection matrix
    let aspect = width / height;
    if (this._projection === enums.PROJ_PERSPECTIVE) {
      Mat4.perspective(_matProj,
        this._fov,
        aspect,
        this._near,
        this._far
      );
    } else {
      let x = this._orthoHeight * aspect;
      let y = this._orthoHeight;
      Mat4.ortho(_matProj,
        -x, x, -y, y, this._near, this._far
      );
    }

    // view-projection
    Mat4.mul(_matViewProj, _matProj, _matView);
    // inv view-projection
    Mat4.invert(_matInvViewProj, _matViewProj);
  }

齐次裁切空间坐标系

GPU经过顶点着色器以后,会把坐标变换到齐次裁切空间坐标系下,此时可以把它想象成一个投影仪,虽然顶点在顶点着色器中乘以一个投影矩阵,但是却没有真正的投影,真正的投影的是齐次除法,就是让顶点坐标都除以w分量,P(x/w,y/w,z/w,w/w) ,在齐次裁切坐标系下,w分量指的是投影仪到屏幕的距离,如果将投影仪靠近屏幕,2D图片缩小,如果投影仪远离屏幕,2D图片放大。没错,这就是 W 分量的作用
所谓的齐次就是加一个维度,对于缩放和旋转,三维矩阵就够了,可是平移需要加一个维度变成四维

假设一个模型上一点的顶点坐标是a[x1,y1,z1,w]
这个模型坐标是b[x0,y0,z0,w]
下面这个是初始矩阵M

x y z w
【1 0 0 0】
【0 1 0 0】
【0 0 1 0】
【0 0 0 1】
下面这个模型上顶点的父矩阵
x y z w
【1 0 0 x0】
【0 1 0 y0】
【0 0 1 z0】
【0 0 0 1】

我们可以将模型上顶点的变换全部记录在这个矩阵中
对于opegl而言它要的是视口坐标,所以在着色器中我们需要将顶点转换到齐次裁切空间坐标系下,而pvm就是我们需要在外部传给他的,m代表模型矩阵,v代表视口矩阵,p代表是投影矩阵
在这里插入图片描述
齐次裁切坐标系的坐标范围是【-1,1】,而屏幕的坐标范围是【0,1】,这个时候需要做一个屏幕映射
齐次除法–》NDC标准的屏幕坐标–>视口转换–》屏幕坐标
根据坐标对齐原则,可以进行对视口的位置进行设置(x,y),可以对视口的大小进行缩放(w,h)

/*
x:起点的横坐标
y:起点的纵坐标
w:宽度比例
h:高度比例
*/
this.gl.viewport(x,y,w*this.gl.viewportWidth,h*this.gl.viewportHeight);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值