The Matrix Resurrections
前言
这是白玉无冰记录3D数学第三篇章,矩阵!往期目录如下:
![](https://filescdn.proginn.com/347b9f0248da1e834ae4d8ede407bc16/e7391306aca652e84bc1e0ceeffd6e08.webp)
在开始唠嗑前,先简单介绍一下标题与配图的含义。
矩阵重启
:重新捡起矩阵的知识MVP
:三个矩阵的简称Matrix 4
: 3D 游戏开发中,常用的是4X4矩阵
![](https://filescdn.proginn.com/4c6c168371223429d459d12f08d4eed4/c81d8d992a45073c9982d1f418c98fcc.webp)
白玉无冰打算围绕 Cocos Creator 3.4
中的源码,展开认识其中用到的矩阵。
![](https://filescdn.proginn.com/7671cf9e2e89bf7fa9f3e66e0ae2c9f1/2268642ee2fc7a1796a230fa00d10b24.webp)
开始
直观感受
矩阵就是一组数字摆成矩形的阵法。
矩阵就是映射!矩阵就是映射!矩阵就是映射!
![](https://filescdn.proginn.com/1f8b3197ddbe09152ab0cb0af731d124/0455d0ecc385cbaa23d48dc1d1b0124f.webp)
摘录《程序员的数学3线性代数》中的内容感受一下矩阵!
![](https://filescdn.proginn.com/9dca6111235e986fe976e340dd057707/f8c7a6c5c8276232cfa3405bb9033717.webp)
矩阵就是映射! m
xn
的矩阵是 n维-> m维
的映射!
![](https://filescdn.proginn.com/3019dd4a83fb2425aa6fe7e5fbeb5fd8/601cc7fd3211cd216a17e50c742dcfe4.webp)
矩阵就是映射! 矩阵的乘积就是映射的叠加!
![](https://filescdn.proginn.com/8f716458b0eb1b02ae49da5f81a59863/224f9a1b228276ead00e14668a101043.webp)
矩阵就是映射! 逆矩阵就是逆映射!好比把水变成冰的过程看作一个矩阵,那么逆矩阵就是冰变成水的过程。
并不是所有的矩阵都有逆矩阵,类似把水果榨成果汁可以做到,但是把果汁还原成水果就不行了。
![](https://filescdn.proginn.com/734d5011e1328c4eeced3ded9c982635/8fea1b302a1bb7c092747371213b7eab.webp)
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矩阵部分截取下来,一并作为参考与思考。
![](https://filescdn.proginn.com/ad50f5ef2d77340acf9bcf866e6beb50/eafb44497c6dbeb0e57d2a717992cc14.webp)
以 engine/editor/assets/chunks/particle-common.chunk
中的代码为例,复习一下矩阵的元素访问。
SRT 与 MV
先看看每个字母的全称:
Scaling 缩放 Rotation 旋转 Translation 位移 Model 模型 View 观察
白玉无冰不打算讲每一个推导,我们直接上号看结果!
![](https://filescdn.proginn.com/4d3ee68b1272a74d800d2b1857e9e0bd/4f347e63bbdba60056b4ee7dd8f90b07.webp)
开始前,先准备一小段代码,当然这段不是很重要,可以快速滑过代码部分,放出来是为了方便复制到自己的工程预览。
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)}`);
}
}
}
}
把上面的组件挂在场景中的一个子节点,开始观察!
![](https://filescdn.proginn.com/aa4b3ff92375cde8e029a07051e09cd1/41cd24c02a21e32ce1e01a13b20f21e8.webp)
Scaling
只对节点缩放,观察世界矩阵。
![](https://filescdn.proginn.com/c10f9f182c590255f5e9e2e7aa62ad12/82999c3b1c1687f6372304d650bff6bf.webp)
再来几个缩放,一起对比!
通过观察得出,缩放矩阵形如
。顺便观察逆矩阵!缩放逆矩阵形如
。最后看一眼引擎源码确认一下,确实如此。
Rotation
只对节点旋转,观察世界矩阵。
![](https://filescdn.proginn.com/9581939fd6741ad09ffb68772fe357c2/10204c694570ce794adec7e84fa3d6c2.webp)
好像没看出什么东西?认真看看世界矩阵与逆矩阵。可以看出这两矩阵是转置关系。即
也就是说,假设某个旋转矩阵是
那么这个旋转矩阵的逆矩阵就会是
🎈 正交矩阵是指其转置等于逆的矩阵。其行列式为±1,行(列)向量组为n维单位正交向量组。
那我们改一个旋转参数试试!
只改 z
(绕Z轴旋转):
![](https://filescdn.proginn.com/25274c1ca1c74ba0ab9fbcfa165dde44/52bf7433d2a0a5f6d65c3da113d47a58.webp)
仔细观察,大胆假设绕z
轴旋转的矩阵应该是:
瞅瞅引擎源码。
![](https://filescdn.proginn.com/575f9f768d126ec9337102b66c1b4f30/3d97c1633a291db8bcc62090b7c574c5.webp)
只改 y
(绕Y轴旋转):
![](https://filescdn.proginn.com/d36098fc2576f9ceeeb4e7ad31d878c1/af2f6017f38f88fa5da451fda433a1d0.webp)
绕y
轴旋转的矩阵应该是:
只改 x
(绕X轴旋转):
![](https://filescdn.proginn.com/d9b568d7e1f2dacf98dc3d3789b82bc1/f47ea02d96c90846b6c086997e1c145c.webp)
绕x
轴旋转的矩阵应该是:
🎈 编辑器中的
Rotation
指的是欧拉角,节点代码中存的是四元数。
![](https://filescdn.proginn.com/7eee3c208351703e8d2c9190f5a2693e/4de56b404793926efe8618061d9e4dbc.webp)
我们把旋转矩阵一直出现的部分提出来
正好是二维中的旋转矩阵。将其相乘两次
大胆推测
,旋转次的,与旋转的结果是相同的。忘了在哪看到复数可以用矩阵表示
把 和 代入上面的式子得到
再根据欧拉公式(可由泰勒展开推出)
可以推出
![](https://filescdn.proginn.com/5e3813692f0d97ad8d09515b445ee2db/be167622f69eeeff93c7a925957cbf22.webp)
这段扯远了,让我们回归正题吧。
旋转矩阵实际上还能与本地坐标系的轴对上!
![](https://filescdn.proginn.com/8fd6a4e48419c903b10d0b2f973e44ad/c07c263dfb673ae691ab7c58d4d01230.webp)
所以,旋转矩阵也可写成相互垂直的单位向量。
其逆矩阵为
Translation
只对节点移动,观察世界矩阵。
![](https://filescdn.proginn.com/257a4f8b2dd7fad8b366c7ce7dcfaef4/ed0a81c3609ab318e58a2a2da61f51e3.webp)
相信我们已经是个成熟的观察者了,容易得出位移矩阵:
其逆矩阵为:
我们还是需要走程序的,看看源码。
Model
Model是SRT的组合!
在编辑器中观察三者关系!
根据观察得出:
M
的第一列 =R
的第一列乘上Sx
M
的第二列 =R
的第二列乘上Sy
M
的第三列 =R
的第三列乘上Sz
M
的第四列 =T
的第四列
![](https://filescdn.proginn.com/545d2e7af29edcb4056913893841d2c2/098f957897fcf75f7fad447227dc171d.webp)
也就是说 Model 矩阵可写成
View
为何把 View 与 Model 放在一起讲?本质上来说他们是互为逆矩阵的关系。
View 矩阵的作用是将世界坐标映射到摄像机坐标。
![](https://filescdn.proginn.com/1b41ffe830c5098231606bd777453df5/f76425614f7c166b3bf5a90ed6c2d723.webp)
在 Cocos Creator 中,摄像机也属于一个节点,View 映射正好是相机节点的 Model 矩阵的反映射。
一般相机不含缩放,所以
引擎源码也是直接用了节点的逆矩阵。
//engine\cocos\core\renderer\scene\camera.ts update
Mat4.invert(this._matView, this._node.worldMatrix);
这里也和 LookAt
矩阵相关,也有说是 UVN
坐标系,但本质上就是求逆矩阵的过程。
![](https://filescdn.proginn.com/4e19abb9b178aabcdaec52ccc9c0bf74/8bc304406c811abfdfc7af485255c18a.webp)
Projection
投影一般指高维向低维的映射,就像我们拍照一样,对现实中的3D物体拍照,在照片上呈现的是2D图像。
![](https://filescdn.proginn.com/620ccec10f59da380516fd4485305c08/fd47163765ec48e552dcbb37902ffac0.webp)
投影矩阵本质上是将视锥体映射到一个正方体上(NDC)。
Projection transformation matrix Mproj: maps World Coordinate values in view volume to Normalized Device Coordinates (NDC) in the range (-1, +1)
![](https://filescdn.proginn.com/638f326478153a87f6ddab7e8085fb5c/2302d7fc22c6cb09af95d0c126009443.webp)
特别注意NDC坐标系是左手系,View中获得的是右手系,计算的时候要翻转Z轴。
对于透视投影(PERSPECTIVE)和正交投影(ORTHO)矩阵的推导,可参考下面的图。
![](https://filescdn.proginn.com/afb380d6bd599408a1dfc7c915ceea9a/82dee0f2de52f8cb8b79531ea27d7b93.webp)
![](https://filescdn.proginn.com/814ac695d18f45e3e17cf01afe5f3f27/b0b2dc0d392f728178275d2fcadc7fd0.webp)
构造透视投影矩阵常用的一种方式是视角(Field-of-View),只需要用三角变换转成视锥体即可。
![](https://filescdn.proginn.com/899ae2357a6f53a4076f2bdc1adbd8f5/b4b4138d3793aa9f4a8d7e0108e73119.webp)
//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);
}
结束
矩阵就是映射,矩阵的乘积就是映射的叠加,逆矩阵就是逆映射!
![](https://filescdn.proginn.com/1caa1fad6710fe0cd27dff3de5fb6770/ce3732d968a3b0cb98e740e1890be387.webp)
参考资料
《程序员的数学3线性代数》 《WebGL编程指南》 https://www.cs.auckland.ac.nz/compsci372s2c/christofLectures/ 《Fundamentals of Computer Graphics, Fourth Edition》
如果你还没有明白,那么就算全世界的人都说‘明白了,很简单啊’,你仍然要鼓起勇气说‘不,我还不明白’。这一点很重要。就算别人再怎么明白,如果自己不明白,那也没有意义。要花时间来思考,思考到理解为止。这样得到的东西就一辈子都属于自己。谁也抢不走,认真学习,细心积累,会带给你自信。 《数学女孩》
往期目录:
更多精彩欢迎关注微信公众号
![](https://filescdn.proginn.com/88268321740866788cf7821a6945de6f/cac4f9703453e613e022c8028e61e5ac.webp)