首页 文章详情

矩阵重启,你就是MVP

白玉无冰 | 1467 2022-02-26 18:46 0 0 0
UniSMS (合一短信)

The Matrix Resurrections

前言

这是白玉无冰记录3D数学第三篇章,矩阵!往期目录如下:

在开始唠嗑前,先简单介绍一下标题与配图的含义。

  • 矩阵重启:重新捡起矩阵的知识
  • MVP:三个矩阵的简称
  • Matrix 4: 3D 游戏开发中,常用的是4X4矩阵

白玉无冰打算围绕 Cocos Creator 3.4 中的源码,展开认识其中用到的矩阵。

开始

直观感受

矩阵就是一组数字摆成矩形的阵法。


矩阵就是映射!矩阵就是映射!矩阵就是映射!

摘录《程序员的数学3线性代数》中的内容感受一下矩阵!

矩阵就是映射

矩阵就是映射! mxn 的矩阵是 n维-> m维 的映射!

n维-> m维

矩阵就是映射! 矩阵的乘积就是映射的叠加!

矩阵的乘积就是映射的叠加

矩阵就是映射! 逆矩阵就是逆映射!好比把水变成冰的过程看作一个矩阵,那么逆矩阵就是冰变成水的过程。

并不是所有的矩阵都有逆矩阵,类似把水果榨成果汁可以做到,但是把果汁还原成水果就不行了。

逆映射

Mat4

engine/cocos/core/math/mat4.tsCocos 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矩阵部分截取下来,一并作为参考与思考。

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》

如果你还没有明白,那么就算全世界的人都说‘明白了,很简单啊’,你仍然要鼓起勇气说‘不,我还不明白’。这一点很重要。就算别人再怎么明白,如果自己不明白,那也没有意义。要花时间来思考,思考到理解为止。这样得到的东西就一辈子都属于自己。谁也抢不走,认真学习,细心积累,会带给你自信。  《数学女孩》

往期目录:

更多精彩欢迎关注微信公众号


➡️【2021年原创精选】

good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter