The Matrix Resurrections
前言
这是白玉无冰记录3D数学第三篇章,矩阵!往期目录如下:
在开始唠嗑前,先简单介绍一下标题与配图的含义。
矩阵重启
:重新捡起矩阵的知识MVP
:三个矩阵的简称Matrix 4
: 3D 游戏开发中,常用的是4X4矩阵
白玉无冰打算围绕 Cocos Creator 3.4
中的源码,展开认识其中用到的矩阵。
开始
直观感受
矩阵就是一组数字摆成矩形的阵法。
矩阵就是映射!矩阵就是映射!矩阵就是映射!
摘录《程序员的数学3线性代数》中的内容感受一下矩阵!
矩阵就是映射! m
xn
的矩阵是 n维-> m维
的映射!
矩阵就是映射! 矩阵的乘积就是映射的叠加!
矩阵就是映射! 逆矩阵就是逆映射!好比把水变成冰的过程看作一个矩阵,那么逆矩阵就是冰变成水的过程。
并不是所有的矩阵都有逆矩阵,类似把水果榨成果汁可以做到,但是把果汁还原成水果就不行了。
Mat4
engine/cocos/core/math/mat4.ts
是 Cocos Creator
引擎表示四维(4x4)矩阵(Mathematical 4x4 matrix.
)。
mat4.ts
构造了一个形如
为何下标是按照列主序构建的呢?因为 Cocos Effect 使用的是 GLSL 语言,矩阵是按照列主序存在数组中的。
Cocos Effect 是一种基于 YAML 和 GLSL 的单源码嵌入式领域特定语言(single-source embedded domain-specific language),YAML 部分声明流程控制清单,GLSL 部分声明实际的 shader 片段,这两部分内容上相互补充,共同构成了一个完整的渲染流程描述。
顺便翻一下《WebGL编程指南》,把GLSL矩阵部分截取下来,一并作为参考与思考。
以 engine/editor/assets/chunks/particle-common.chunk
中的代码为例,复习一下矩阵的元素访问。
SRT 与 MV
先看看每个字母的全称:
Scaling 缩放 Rotation 旋转 Translation 位移 Model 模型 View 观察
白玉无冰不打算讲每一个推导,我们直接上号看结果!
开始前,先准备一小段代码,当然这段不是很重要,可以快速滑过代码部分,放出来是为了方便复制到自己的工程预览。
import { _decorator, Component, Node, mat4, Mat4 } from 'cc';
const { ccclass, property, executeInEditMode } = _decorator;
const __temp_mat4 = mat4();
@ccclass('NodeMatrixInfo')
@executeInEditMode
export class NodeMatrixInfo extends Component {
@property({ readonly: true, visible: true, displayName: '世界矩阵' })
__txt_matrix: string[] = [];
@property({ readonly: true, visible: true, displayName: '三角函数' })
__txt_sin_cos: string[] = [];
@property({ readonly: true, visible: true, displayName: '世界矩阵的逆' })
__txt_matrixInvert: string[] = [];
@property({ readonly: true, visible: true, displayName: '欢迎关注' })
__txt_info: string = '白玉无冰';
start() {
this.node.on(Node.EventType.TRANSFORM_CHANGED, this.onNodeTransFormChange, this);
this.onNodeTransFormChange();
}
private onNodeTransFormChange(evt?) {
const {
m00: m00, m04: m01, m08: m02, m12: m03,
m01: m10, m05: m11, m09: m12, m13: m13,
m02: m20, m06: m21, m10: m22, m14: m23,
m03: m30, m07: m31, m11: m32, m15: m33
} = this.node.getWorldMatrix(__temp_mat4);
this.__txt_matrix[0] = `${m00.toFixed(2)}, ${m01.toFixed(2)}, ${m02.toFixed(2)}, ${m03.toFixed(2)}`;
this.__txt_matrix[1] = `${m10.toFixed(2)}, ${m11.toFixed(2)}, ${m12.toFixed(2)}, ${m13.toFixed(2)}`;
this.__txt_matrix[2] = `${m20.toFixed(2)}, ${m21.toFixed(2)}, ${m22.toFixed(2)}, ${m23.toFixed(2)}`;
this.__txt_matrix[3] = `${m30.toFixed(2)}, ${m31.toFixed(2)}, ${m32.toFixed(2)}, ${m33.toFixed(2)}`;
{
const {
m00: m00, m04: m01, m08: m02, m12: m03,
m01: m10, m05: m11, m09: m12, m13: m13,
m02: m20, m06: m21, m10: m22, m14: m23,
m03: m30, m07: m31, m11: m32, m15: m33
} = Mat4.invert(__temp_mat4, __temp_mat4);
this.__txt_matrixInvert[0] = `${m00.toFixed(2)}, ${m01.toFixed(2)}, ${m02.toFixed(2)}, ${m03.toFixed(2)}`;
this.__txt_matrixInvert[1] = `${m10.toFixed(2)}, ${m11.toFixed(2)}, ${m12.toFixed(2)}, ${m13.toFixed(2)}`;
this.__txt_matrixInvert[2] = `${m20.toFixed(2)}, ${m21.toFixed(2)}, ${m22.toFixed(2)}, ${m23.toFixed(2)}`;
this.__txt_matrixInvert[3] = `${m30.toFixed(2)}, ${m31.toFixed(2)}, ${m32.toFixed(2)}, ${m33.toFixed(2)}`;
}
{
const eulerAngles = this.node.eulerAngles;
const angle2rad = 1 / 180 * Math.PI;
this.__txt_sin_cos = [
`sin ${eulerAngles.x} = ${Math.sin(eulerAngles.x * angle2rad).toFixed(2)}`,
`cos ${eulerAngles.x} = ${Math.cos(eulerAngles.x * angle2rad).toFixed(2)}`,
]
if (eulerAngles.y != eulerAngles.x) {
this.__txt_sin_cos.push(
`sin ${eulerAngles.y} = ${Math.sin(eulerAngles.y * angle2rad).toFixed(2)}`,
`cos ${eulerAngles.y} = ${Math.cos(eulerAngles.y * angle2rad).toFixed(2)}`);
}
if (eulerAngles.z != eulerAngles.y && eulerAngles.z != eulerAngles.x) {
this.__txt_sin_cos.push(
`sin ${eulerAngles.z} = ${Math.sin(eulerAngles.z * angle2rad).toFixed(2)}`,
`cos ${eulerAngles.z} = ${Math.cos(eulerAngles.z * angle2rad).toFixed(2)}`);
}
}
}
}
把上面的组件挂在场景中的一个子节点,开始观察!
Scaling
只对节点缩放,观察世界矩阵。
再来几个缩放,一起对比!
通过观察得出,缩放矩阵形如
。顺便观察逆矩阵!缩放逆矩阵形如
。最后看一眼引擎源码确认一下,确实如此。
Rotation
只对节点旋转,观察世界矩阵。
好像没看出什么东西?认真看看世界矩阵与逆矩阵。可以看出这两矩阵是转置关系。即
也就是说,假设某个旋转矩阵是
那么这个旋转矩阵的逆矩阵就会是
🎈 正交矩阵是指其转置等于逆的矩阵。其行列式为±1,行(列)向量组为n维单位正交向量组。
那我们改一个旋转参数试试!
只改 z
(绕Z轴旋转):
仔细观察,大胆假设绕z
轴旋转的矩阵应该是:
瞅瞅引擎源码。
只改 y
(绕Y轴旋转):
绕y
轴旋转的矩阵应该是:
只改 x
(绕X轴旋转):
绕x
轴旋转的矩阵应该是:
🎈 编辑器中的
Rotation
指的是欧拉角,节点代码中存的是四元数。
我们把旋转矩阵一直出现的部分提出来
正好是二维中的旋转矩阵。将其相乘两次
大胆推测
,旋转次的,与旋转的结果是相同的。忘了在哪看到复数可以用矩阵表示
把 和 代入上面的式子得到
再根据欧拉公式(可由泰勒展开推出)
可以推出
这段扯远了,让我们回归正题吧。
旋转矩阵实际上还能与本地坐标系的轴对上!
所以,旋转矩阵也可写成相互垂直的单位向量。
其逆矩阵为
Translation
只对节点移动,观察世界矩阵。
相信我们已经是个成熟的观察者了,容易得出位移矩阵:
其逆矩阵为:
我们还是需要走程序的,看看源码。
Model
Model是SRT的组合!
在编辑器中观察三者关系!
根据观察得出:
M
的第一列 =R
的第一列乘上Sx
M
的第二列 =R
的第二列乘上Sy
M
的第三列 =R
的第三列乘上Sz
M
的第四列 =T
的第四列
也就是说 Model 矩阵可写成
View
为何把 View 与 Model 放在一起讲?本质上来说他们是互为逆矩阵的关系。
View 矩阵的作用是将世界坐标映射到摄像机坐标。
在 Cocos Creator 中,摄像机也属于一个节点,View 映射正好是相机节点的 Model 矩阵的反映射。
一般相机不含缩放,所以
引擎源码也是直接用了节点的逆矩阵。
//engine\cocos\core\renderer\scene\camera.ts update
Mat4.invert(this._matView, this._node.worldMatrix);
这里也和 LookAt
矩阵相关,也有说是 UVN
坐标系,但本质上就是求逆矩阵的过程。
Projection
投影一般指高维向低维的映射,就像我们拍照一样,对现实中的3D物体拍照,在照片上呈现的是2D图像。
投影矩阵本质上是将视锥体映射到一个正方体上(NDC)。
Projection transformation matrix Mproj: maps World Coordinate values in view volume to Normalized Device Coordinates (NDC) in the range (-1, +1)
特别注意NDC坐标系是左手系,View中获得的是右手系,计算的时候要翻转Z轴。
对于透视投影(PERSPECTIVE)和正交投影(ORTHO)矩阵的推导,可参考下面的图。
构造透视投影矩阵常用的一种方式是视角(Field-of-View),只需要用三角变换转成视锥体即可。
//engine\cocos\core\renderer\scene\camera.ts update
// this._aspect 宽高比
if (this._proj === CameraProjection.PERSPECTIVE) {
// 透视
// out: IMat4Like, fov: number, aspect: number, near: number, far: number,
// isFOVY = true, minClipZ = -1, projectionSignY = 1, orientation = 0,
Mat4.perspective(this._matProj, this._fov, this._aspect, this._nearClip, this._farClip,
this._fovAxis === CameraFOVAxis.VERTICAL, this._device.capabilities.clipSpaceMinZ, projectionSignY, orientation);
} else {
// 正交
const x = this._orthoHeight * this._aspect;
const y = this._orthoHeight;
// out: IMat4Like, left: number, right: number, bottom: number, top: number, near: number, far: number,
// minClipZ = -1, projectionSignY = 1, orientation = 0,
Mat4.ortho(this._matProj, -x, x, -y, y, this._nearClip, this._farClip,
this._device.capabilities.clipSpaceMinZ, projectionSignY, orientation);
}
结束
矩阵就是映射,矩阵的乘积就是映射的叠加,逆矩阵就是逆映射!
参考资料
《程序员的数学3线性代数》 《WebGL编程指南》 https://www.cs.auckland.ac.nz/compsci372s2c/christofLectures/ 《Fundamentals of Computer Graphics, Fourth Edition》
如果你还没有明白,那么就算全世界的人都说‘明白了,很简单啊’,你仍然要鼓起勇气说‘不,我还不明白’。这一点很重要。就算别人再怎么明白,如果自己不明白,那也没有意义。要花时间来思考,思考到理解为止。这样得到的东西就一辈子都属于自己。谁也抢不走,认真学习,细心积累,会带给你自信。 《数学女孩》
往期目录:
更多精彩欢迎关注微信公众号