本地矩阵和世界矩阵
显示节点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);