Unity 不同空间坐标转换中的矩阵应用

前言

本文章主要介绍通过矩阵的方式实现对象的空间变换,主要涉及的变换形式为世界空间转相对空间中的矩阵实现过程

实际开发中,Unity已经为开发者封装好一些坐标转换的方法,使用起来是比较方便的,但是对于理解而言就比较复杂了,所以下面的介绍会基于模拟的方式来了解Unity的矩阵坐标变换过程

一、通过向量进行空间变换

在开始前先做一点准备工作,创建两个空物体分别命名为centerPostargetPos

接下来所执行的操作就可以理解为求targetPos相对于centerPos的本地坐标系下的坐标,即此时centerPos为坐标原点,来求出targetPos在该坐标系下的坐标位置

为了便于观察,通过Gizmos绘制出centerPos的本地坐标系,同时标识出两者的位置关系,如图所示:
在这里插入图片描述

为了便于后续矩阵的理解,先通过常规的方式来计算得到targetPos相对于centerPos的本地坐标位置,实现过程很简单,分为两步来描述:

  • 得到从centerPos指向targetPos的向量
  • 计算该向量在centerPos坐标系下各个轴的投影

得到一个向量可以通过终止坐标点减去起始坐标点获取,而投影就更简单了,只需要与centerPos本地坐标系的各个轴的向量做一个点积即可,经过计算就可以得到转换坐标系后的坐标位置,代码为:

    public Vector3 GetTargetRelatPos(Transform targetPos , Transform centerPos)
    {
        Vector3 result=new Vector3(0,0,0);
        Vector3 v3 = targetPos.position - centerPos.position;
        result.x = Vector3.Dot(v3 , centerPos.right);
        result.y = Vector3.Dot(v3 , centerPos.up);
        result.z = Vector3.Dot(v3 , centerPos.forward);
        return result;
    }

直接通过代码解释比较抽象,而这部分恰巧又与后面坐标转换矩阵的内容相关,所以这里来将其可视化一下:
在这里插入图片描述
图中各个对象的信息为:

  • 绿色小球:中心点centerPos与目标点targetPos
  • 红、绿、蓝三条辅助线:centerPos的本地坐标系的三个轴
  • 黄色辅助线:从centerPos指向targetPos的向量
  • 白色辅助线:代表向量投影坐标轴的垂线
  • 白色小球:代表向量在各个轴上的投影点

绘制的代码为,可以在项目中实际操作一下,转换角度来理解:

	public void OnDrawGizmos()
    {
        //绿色小球绘制
        Gizmos.color = Color.green;
        Gizmos.DrawSphere(centerPos.position, 0.1f);
        Gizmos.DrawSphere(targetPos.position, 0.1f);
        //坐标辅助线绘制
        Gizmos.color=Color.red;
        Gizmos.DrawLine(centerPos.position, centerPos.position+centerPos.right*10);  
        Gizmos.color=Color.green;
        Gizmos.DrawLine(centerPos.position, centerPos.position+centerPos.up*10);  
        Gizmos.color=Color.blue;
        Gizmos.DrawLine(centerPos.position, centerPos.position+centerPos.forward*10);
        //向量绘制
        Gizmos.color = Color.yellow;
        Gizmos.DrawLine(centerPos.position, targetPos.position);

        Vector3 v3 = GetTargetRelatPos(targetPos,centerPos);
        //垂线绘制
        Gizmos.color = Color.white;
        Gizmos.DrawLine(targetPos.position, centerPos.position+ v3.x * centerPos.right); 
        Gizmos.DrawLine(targetPos.position, centerPos.position+ v3.y * centerPos.up);
        Gizmos.DrawLine(targetPos.position, centerPos.position+ v3.z * centerPos.forward);
        //小球绘制
        Gizmos.DrawSphere(centerPos.position + v3.x * centerPos.right, 0.1f);
        Gizmos.DrawSphere(centerPos.position + v3.y * centerPos.up, 0.1f);
        Gizmos.DrawSphere(centerPos.position + v3.z * centerPos.forward, 0.1f);
    }

通过对上面的代码的解析,可以看出影响物体坐标转换的关键因素在于参考点centerPos的坐标位置与其旋转关系,这里通过一个动图来简单的解释一下:

在这里插入图片描述
通过图中演示坐标系初始位置、旋转、缩放三个维度变化对于最终结果的影响可以看出,坐标系初始位置的变化会引起向量的模的变化,而旋转则会改变向量在各个坐标轴的分量,即局部空间下的向量方向的改变,那么我们很容易就总结出:

  • 影响物体局部坐标的关键因素在于初始点的位置与其方向,这里的方向可以拆分为三个坐标轴rightupforward,而缩放则不会引起坐标转换过程中位置的变化

二、使用空间变换矩阵

同样对于上面的一个案例,通过矩阵的方式来计算得到targetPos相对于centerPos坐标系的局部坐标,为了便于理解,我们先排除缩放的影响,默认centerPos的缩放为(1,1,1),则可以通过下面几种方式完成计算:

	//第一种:
	public Vector3 GetTargetRelatPosFirst(Transform targetPos, Transform centerPos)
    {      
        return centerPos.InverseTransformPoint(targetPos.position);
    }
 	//第二种:
    public Vector3 GetTargetRelatPosSencond(Transform targetPos, Transform centerPos)
    {       
        Matrix4x4 m4 = centerPos.worldToLocalMatrix;
        return m4.MultiplyPoint3x4(targetPos.position);
    }
    //第三种:
    public Vector3 GetTargetRelatPosThird(Transform targetPos, Transform centerPos)
    {
        Vector4 v4 = new Vector4(targetPos.position.x, targetPos.position.y, targetPos.position.z, 1);
        return centerPos.worldToLocalMatrix * v4;
    }
	

几种方式的实现过程大致相同,都是基于矩阵乘法的坐标转换,不过第一种与第二种是Unity官方对矩阵与三维向量乘法的一个进一步封装,而第三种的实现过程相对完整,这里就根据第二种的计算方法来理解,大概可以分为下面几个环节:

1、齐次坐标转换:

通过第三种解决方案的代码可以看到,会将targetPosPositionVector3变换为Vector4,并对最后的一维补充为1。通过高中数学知识可以了解到,向量可以转换成为矩阵,而对于矩阵的乘法而言,第一个矩阵的列数必须与第二个矩阵的行数相同,所以这里为了可以进行计算,需要将三维向量转换为四维

但是最后一个1并不是单纯的补位,在矩阵的乘法中也有着重要的计算意义,比如说,如果我们不手动将Vector3变换为Vector4Unity也会默认帮我们处理变换,但是最后补充的就是数字0,然后你就会惊奇的发现计算的结果与实际结果有一定的偏差

这里是齐次坐标转换规则的原因,一般来说,在坐标转换矩阵中,会将对象的坐标运算从笛卡尔坐标空间转换到齐次坐标空间内。而在齐次坐标空间内0代表向量,而1代表的是坐标点,而由于我们 通过转换的矩阵是直接通过目标点targetPos的世界坐标来计算的,而不像我们初始实现的那样,通过centerPostargetPos的向量来执行计算,所以这里要补充的是1,代表该vector3表示的是targetPos的世界坐标

由于该位是1,就会在后续的矩阵乘法计算中加上偏移量,也就是centerPos的世界坐标点,代表的其相对于世界坐标(0,0,0)的偏移量,有点难以理解,但是到后面的矩阵计算时就会一目了然了

2、理解TRS矩阵

TRS矩阵,也就是由TranslateRotateScale组合而成的矩阵,并且矩阵的变换顺序为先Scale,然后Rotate,最后再进行Translate的操作,这样的执行顺序是为了避免由于位移与旋转的影响而产生缩放的形变问题,所以先执行缩放的操作,而TRS的执行顺序可以理解为:

  • T(R(Sp)))

要完成TRS矩阵的生成转发,首先要理解TRS矩阵所构成的位置、旋转、缩放分别对应的基本矩阵,所以这里首先拆解一下各个矩阵:

Translate:

基于单位矩阵进行变化,位置信息会被记录在4x4矩阵的m01m02m03位置,在Unity中可以通过Matrix4x4.Translate()打印出位置矩阵。这里通过设置物体坐标位置(1,2,3)并通过Debug.Log(Matrix4x4.Translate(centerPos.position));打印出centerPos对应的位置矩阵,结果如图所示:

在这里插入图片描述
Translate的矩阵表示为:
{ 1 0 0 T x 0 1 0 T y 0 0 1 T z 0 0 0 1 } \left\{ \begin{matrix} 1 & 0 & 0 & T_x\\ 0 & 1 & 0 & T_y\\ 0 & 0 & 1 & T_z \\ 0 &0 &0 &1 \end{matrix} \right\} 100001000010TxTyTz1

Rotate:

旋转矩阵本身需要3x3矩阵,该矩阵记录了对象本地坐标的三个坐标轴的单位向量,即centerPos.RightcenterPos.Up以及centerPos.Forward三个坐标轴的向量,为centerPos设置一定的旋转量,并打印出结果,实施打印的代码为:

        Debug.Log(Matrix4x4.Rotate(centerPos.rotation));
        Debug.Log(centerPos.right);
        Debug.Log(centerPos.up);
        Debug.Log(centerPos.forward);

打印结果,如图:
在这里插入图片描述
可以看出矩阵数值与对象的局部坐标轴的单位向量相互对应,即对象的旋转矩阵表示为:

{ R x U x F x 0 R y U y F y 0 R z U z F z 0 0 0 0 1 } \left\{ \begin{matrix} R_ x & U_x & F_x & 0\\ R_y & U_y & F_y & 0\\ R_z & U_z & F_z & 0 \\ 0 &0 &0 &1 \end{matrix} \right\} RxRyRz0UxUyUz0FxFyFz00001

其中R对应Right,而U对应UpF则对应着Forward,代表Unity本地坐标轴的三个分量

Scale:

关于剩下的缩放的矩阵,最简单的想法是可以通过剩下的m30m31m32来记录,但是事实并不是如此,为了方便位移旋转与缩放的组合计算得到最后TRS矩阵,需要进行矩阵的乘法计算,同时缩放的三个分量分别对应旋转的三个坐标轴分量,为了在乘法将缩放与旋转结合起来,就必须使缩放矩阵与旋转矩阵相似,使其分量可以产生乘法计算

同样是基于3x3的矩阵来,设置centerPos的缩放为(2,3,4),并通过 Debug.Log(Matrix4x4.Scale(centerPos.localScale));打印出对应的缩放矩阵,如图所示:
在这里插入图片描述
则可以推断出来缩放的矩阵为:
{ S x 0 0 0 0 S y 0 0 0 0 S z 0 0 0 0 1 } \left\{ \begin{matrix} S_x & 0 & 0 & 0\\ 0 & S_y & 0 & 0\\ 0 & 0 & S_z & 0 \\ 0 &0 &0 &1 \end{matrix} \right\} Sx0000Sy0000Sz00001


得到对象的位移、旋转、缩放矩阵后,可以进行乘法计算得到对象的TRS矩阵,当然也可以直接通过Unity的封装的方法TRS(Vector3 pos, Quaternion q, Vector3 s)来直接得到对象的TRS矩阵,两种方式的代码示例:

		Matrix4x4 trsOne=Matrix4x4.TRS(centerPos.position, centerPos.rotation, centerPos.localScale);
        Matrix4x4 trsTwo = Matrix4x4.Translate(centerPos.position) * Matrix4x4.Rotate(centerPos.rotation) * Matrix4x4.Scale(centerPos.localScale);    

根据上面的代码,以及每个乘法元素对应的矩阵结构来执行乘法的计算,通过矩阵的乘法来计算得到最后的矩阵,乘法的公式构成即TranslateRoateScale三个对应的基本转换矩阵,示例如下:

{ 1 0 0 T x 0 1 0 T y 0 0 1 T z 0 0 0 1 } ∗ { R x U x F x 0 R y U y F y 0 R z U z F z 0 0 0 0 1 } ∗ { S x 0 0 0 0 S y 0 0 0 0 S z 0 0 0 0 1 } \left\{ \begin{matrix} 1 & 0 & 0 & T_x\\ 0 & 1 & 0 & T_y\\ 0 & 0 & 1 & T_z \\ 0 &0 &0 &1 \end{matrix} \right\} * \left\{ \begin{matrix} R_ x & U_x & F_x & 0\\ R_y & U_y & F_y & 0\\ R_z & U_z & F_z & 0 \\ 0 &0 &0 &1 \end{matrix} \right\} *\left\{ \begin{matrix} S_x & 0 & 0 & 0\\ 0 & S_y & 0 & 0\\ 0 & 0 & S_z & 0 \\ 0 &0 &0 &1 \end{matrix} \right\} 100001000010TxTyTz1 RxRyRz0UxUyUz0FxFyFz00001 Sx0000Sy0000Sz00001

经过矩阵的乘法运算得到结果就是对象的TRS矩阵,其具体结构为:
{ R x S x U x S y F x S z T x R y S x U y S y F y S z T y R z S x U z S y F z S z T z 0 0 0 1 } \left\{ \begin{matrix} R_xS_x & U_xS_y & F_xS_z & T_x\\ R_yS_x & U_yS_y & F_yS_z & T_y\\ R_zS_x & U_z S_y & F_zS_z & T_z \\ 0 &0 &0 &1 \end{matrix} \right\} RxSxRySxRzSx0UxSyUySyUzSy0FxSzFySzFzSz0TxTyTz1
通过计算过程与结果来看,Unity中的TRS矩阵是以列为主导的,前三行的前三列分别代表对象的坐标轴的单位向量与各个方向缩放的乘积,而剩下第四行用来表示位置信息

同时需要注意的是,虽然整个乘法过程看起来像是先旋转,但是要明白的是Unity对于矩阵与向量的乘法是右乘,而且矩阵乘法的先后顺序不影响计算结果,所以我们可以反向理解该乘法过程为:

  • Translate 乘 (Rotate 乘 (ScaleVector4)))
3、世界转局部的转换矩阵

矩阵推导:

在前面已经描述了一些实现方法,这里主要是来说明一下矩阵变换的过程,Unity中默认提供给我们的是worldToLocalMatrix 这个空间转换矩阵,如果我们通过改变对象的位移、旋转、缩放,就会发现其生成的变换矩阵与TRS类似,但是又有不同,简单示例:当我们只修改物体的旋转量后,可以发现得到的矩阵是行主导的,如图所示:

在这里插入图片描述

那这里就是对于旋转矩阵做了一个转置操作,即旋转对应的矩阵为旋转矩阵的转置矩阵,用代码表示为Matrix4x4.Transpose(Matrix4x4.Rotate(centerPos.rotation)),矩阵为:
{ R x R y R z 0 U x U y U z 0 F x F y F z 0 0 0 0 1 } \left\{ \begin{matrix} R_ x & R_y & R_z & 0\\ U_x & U_y & U_z & 0\\ F_x & F_y & F_z & 0 \\ 0 &0 &0 &1 \end{matrix} \right\} RxUxFx0RyUyFy0RzUzFz00001

对于位移,同样单独修改centerPos的坐标为(2,3,4),可以观察到打印出的矩阵为位置矩阵的反矩阵,即Matrix4x4.Translate(centerPos.position).inverse,打印的结果如图:
在这里插入图片描述

所以Translate对应的矩阵为:
{ 1 0 0 − T x 0 1 0 − T y 0 0 1 − T z 0 0 0 1 } \left\{ \begin{matrix} 1 & 0 & 0 & -T_x\\ 0 & 1 & 0 & -T_y\\ 0 & 0 & 1 & -T_z \\ 0 &0 &0 &1 \end{matrix} \right\} 100001000010TxTyTz1

最后一步就是对于Scale执行修改,并观察结果,先说结论,ScaleTranslate相同,同样是进行了逆矩阵的操作,但是由于直接观察结果难以察觉到矩阵与逆矩阵的操作,所以我们直接通过打印两个矩阵进行数据对比:

        Debug.Log(centerPos.worldToLocalMatrix);
        Debug.Log(Matrix4x4.Scale(centerPos.localScale).inverse);

执行代码,就会发现两个日志的结果相同,证明了世界坐标转局部坐标通过缩放的逆矩阵来实现,则缩放的矩阵为:

{ 1 / S x 0 0 0 0 1 / S y 0 0 0 0 1 / S z 0 0 0 0 1 } \left\{ \begin{matrix} 1/S_x & 0 & 0 & 0\\ 0 & 1/S_y & 0 & 0\\ 0 & 0 & 1/S_z & 0 \\ 0 &0 &0 &1 \end{matrix} \right\} 1/Sx00001/Sy00001/Sz00001

得到TranslateRotateScale的变换矩阵后,就可以对其执行乘法操作,但是与TRS不同的是,该矩阵的顺序为SRT,代码为:

	Matrix4x4.Scale(centerPos.localScale).inverse * Matrix4x4.Transpose(Matrix4x4.Rotate(centerPos.rotation)) * Matrix4x4.Translate(centerPos.position).inverse;   
	

根据矩阵的乘法,我们当然也可以得到最后的计算结果:

{ R x / S x R y / S x R z / S x − ( R x T x + R y T y + R z T z ) / S x U x / S y U y / S y U z / S y − ( U x T x + U y T y + U z T z ) / S y F x / S z F y / S z F z / S z − ( F x T x + F y T y + F z T z ) / S z 0 0 0 1 } \left\{ \begin{matrix} R_ x/S_x & R_y/S_x & R_z/S_x & -(R_xT_x+R_yT_y+R_zT_z)/S_x\\ U_x/S_y & U_y/S_y & U_z/S_y & -(U_xT_x+U_yT_y+U_zT_z)/S_y\\ F_x/S_z & F_y/S_z & F_z/S_z & -(F_xT_x+F_yT_y+F_zT_z)/S_z \\ 0 &0 &0 &1 \end{matrix} \right\} Rx/SxUx/SyFx/Sz0Ry/SxUy/SyFy/Sz0Rz/SxUz/SyFz/Sz0(RxTx+RyTy+RzTz)/Sx(UxTx+UyTy+UzTz)/Sy(FxTx+FyTy+FzTz)/Sz1

消除缩放的影响:

我们前面说到,对象的空间坐标转换,往往是刚体的转换,即位置与旋转,但是直接使用worldToLocalMatrix的话,对象的缩放会对转换的结果产生影响,所以这里需要消除缩放对于空间转换的影响,直接对worldToLocalMatrix乘上缩放的矩阵,这样就可以将公式内的缩放的逆矩阵转换为单位矩阵:

    public Vector3 GetTargetRelatPosFirst(Transform targetPos, Transform centerPos)
    {       
        Matrix4x4 m4 = centerPos.worldToLocalMatrix*Matrix4x4.Scale(centerPos.localScale);
        return m4.MultiplyPoint3x4(targetPos.position);
    }
总结

掌握好矩阵的计算,可以很好的理解到空间TranslateRotateScale的变换的过程,虽然不理解矩阵也可以通过向量或者四元数来完成所有操作,但是在执行复杂的变换操作时相对就比较难以理解,所以学习认识矩阵还是有一定的帮助的

  • 11
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心之凌儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值