前言
本文章主要介绍通过矩阵的方式实现对象的空间变换,主要涉及的变换形式为世界空间转相对空间中的矩阵实现过程
实际开发中,Unity
已经为开发者封装好一些坐标转换的方法,使用起来是比较方便的,但是对于理解而言就比较复杂了,所以下面的介绍会基于模拟的方式来了解Unity
的矩阵坐标变换过程
一、通过向量进行空间变换
在开始前先做一点准备工作,创建两个空物体分别命名为centerPos
与targetPos
接下来所执行的操作就可以理解为求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
的坐标位置与其旋转关系,这里通过一个动图来简单的解释一下:
通过图中演示坐标系初始位置、旋转、缩放三个维度变化对于最终结果的影响可以看出,坐标系初始位置的变化会引起向量的模的变化,而旋转则会改变向量在各个坐标轴的分量,即局部空间下的向量方向的改变,那么我们很容易就总结出:
- 影响物体局部坐标的关键因素在于初始点的位置与其方向,这里的方向可以拆分为三个坐标轴
right
、up
、forward
,而缩放则不会引起坐标转换过程中位置的变化
二、使用空间变换矩阵
同样对于上面的一个案例,通过矩阵的方式来计算得到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、齐次坐标转换:
通过第三种解决方案的代码可以看到,会将targetPos
的Position
从Vector3
变换为Vector4
,并对最后的一维补充为1。通过高中数学知识可以了解到,向量可以转换成为矩阵,而对于矩阵的乘法而言,第一个矩阵的列数必须与第二个矩阵的行数相同,所以这里为了可以进行计算,需要将三维向量转换为四维
但是最后一个1并不是单纯的补位,在矩阵的乘法中也有着重要的计算意义,比如说,如果我们不手动将Vector3
变换为Vector4
,Unity
也会默认帮我们处理变换,但是最后补充的就是数字0,然后你就会惊奇的发现计算的结果与实际结果有一定的偏差
这里是齐次坐标转换规则的原因,一般来说,在坐标转换矩阵中,会将对象的坐标运算从笛卡尔坐标空间转换到齐次坐标空间内。而在齐次坐标空间内0代表向量,而1代表的是坐标点,而由于我们 通过转换的矩阵是直接通过目标点targetPos
的世界坐标来计算的,而不像我们初始实现的那样,通过centerPos
到targetPos
的向量来执行计算,所以这里要补充的是1,代表该vector3
表示的是targetPos
的世界坐标
由于该位是1,就会在后续的矩阵乘法计算中加上偏移量,也就是centerPos
的世界坐标点,代表的其相对于世界坐标(0,0,0)的偏移量,有点难以理解,但是到后面的矩阵计算时就会一目了然了
2、理解TRS矩阵
TRS
矩阵,也就是由Translate
、Rotate
、Scale
组合而成的矩阵,并且矩阵的变换顺序为先Scale
,然后Rotate
,最后再进行Translate
的操作,这样的执行顺序是为了避免由于位移与旋转的影响而产生缩放的形变问题,所以先执行缩放的操作,而TRS
的执行顺序可以理解为:
- T(R(Sp)))
要完成TRS矩阵的生成转发,首先要理解TRS
矩阵所构成的位置、旋转、缩放分别对应的基本矩阵,所以这里首先拆解一下各个矩阵:
Translate:
基于单位矩阵进行变化,位置信息会被记录在4x4矩阵的m01
、m02
、m03
位置,在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.Right
和centerPos.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\} ⎩ ⎨ ⎧RxRyRz0UxUyUz0