特效笔记 -- 搞定坐标系变换_左乘_右乘_行主序_列主序的倒数第二篇文章

前言

笔者是一个游戏行业的程序员,读书的时候做的基本都是native graphic library的项目,工作了反而对渲染管线细节接触少很多,说到底还是现在的商业引擎都太好用了啊喂。目前绝大多数的渲染算法在github,shader toy上都能找到非常完整的实现,大量的内置函数和宏隐藏了复杂的渲染细节。假如你需要修改unity自带的复杂pbr光照,或者实现一个简单的billboard shader,做为程序员为了能继续方便的打磨别人的轮子,搞明白这些内置函数&变量的数据计算过程就成了必要条件。

这篇文章包含了左&右手坐标系 行主序(row major) 列主序(column major)左乘 右乘 坐标系变换的相关推导和使用注意事项。

相信我,我高考数学选择错了一半都能搞懂,你肯定也可以。

左手坐标系&右手坐标系

左手坐标系是指在空间直角坐标系中,让左手拇指指向x轴的正方向,食指指向y轴的正方向,如果中指能指向z轴的正方向,则称这个坐标系为左手直角坐标系。反之则是右手直角坐标系

left_hand_坐标系.jpg

定义左右手坐标系的作用:


在三维世界中,我们给定一个平面XOY,针对这个平面的旋转角度θ就产生两种情况--顺时针和逆时针。所以对坐标系使用左手与右手的命名,这种命名规则的作用就是用来方便判断旋转的正方向,这就是左手法则和右手法则。

针对上图来说,在左手坐标系下,XOY面的旋转正方向这样获得:大拇指朝向z轴正方向,四指弯曲的方向就是左手坐标系下,旋转的正方向。所以本文的所有旋转在给定左右手坐标系的情况下,旋转角度θ,就是沿着当前坐标系的正方向进行旋转θ角度。

测试可知:左手坐标下,顺时针就是旋转的正方向,右手坐标系则正好相反

left_hand_坐标系_标记.jpg

ps:unity就是基于左手坐标系的,我们可以通过简单的代码进行观察物体是否沿着XOZ面顺时针旋转

    transform.rotation = Quaternion.Euler(0, 45, 0);

1. 点,向量,齐次坐标

为了理解简单点,本文大部分都会先考虑二维的情况,三维世界的情况其实就是二维世界的扩展

在二维世界中,点P&向量V的定义:

p=\left\{x, y, 1\right\} v=\left\{x, y, 0\right\}

这里你可能会提出两个问题:

  1. 为什么要用三维向量去表示二维世界的P&V
  2. 为什么P的第三维度值是1,而V的第三维度值是0

1.1 二维坐标系的Rotate Translate Scale Formula

要回答这些问题我们需要考虑这样的问题,在二维世界中如何对点P完成平移(translate)旋转(rotate)以及缩放(scale)操作。

考虑下图中的点P移动到P',有公式如下:

translation.jpg

考虑下图中的点P旋转到P'-- 在左手坐标系下旋转角度θ的计算公式

rotation.png

接下來我們考虑缩放的情况,对二维坐标系内上X,Y轴分别进行放缩Sx,Sy,计算公式如下:

1.2 二维坐标系的Rotate Translate Scale Matrix

我们接下来考虑一个问题,如何方便的把RTS运算结合在一起呢?答案就是矩阵。原因很简单,矩阵运算天生满足结合律,我们把上述公式转换为矩阵运算后就可以用一个矩阵来表示RTS行为了,这为以后的复杂运算提供了便利。

根据上述的公式,我们可以很简单的得到Rotate Matrix&Scale Matrix

Rotate Matirx in left hand coordinate

Scale Matirx in left hand coordinate

在n维坐标系中,平移矩阵需要n+1维的向量完成平移操作。所谓的齐次坐标就是就是将一个原本是n维的向量用一个n+1维向量来表示

Translate Matirx in left hand coordinate
\left[ \begin{matrix} x' \\ y' \\ 1 \end{matrix} \right ] = \left[ \begin{matrix} 1 & 0 & T_x \\ 0 & 1 & T_y \\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} x\\ y \\ 1 \end{matrix} \right ]

注意:以上的计算公式计算结果都是在同一个坐标系内部的,当我们使用XOY为basic coordinate的情况下,以上公式的计算结果都是在basic coordinate下的

进而我们把旋转矩阵和平移矩阵也引入齐次坐标来使得Rts Matrix可以结合,公式为:
Rotate Matirx in left hand coordinate
\left[ \begin{matrix} x'\\ y' \\ 1 \end{matrix} \right] = \left[ \begin{matrix} cosθ & sinθ & 0 \\ -sinθ & cosθ & 0 \\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} x\\ y \\ 1 \end{matrix} \right]

Scale Matirx in left hand coordinate
\left[ \begin{matrix} x' \\ y' \\ 1 \end{matrix} \right] = \left[ \begin{matrix} R_x & 0 & 0 \\ 0 & R_y & 0 \\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} x\\ y \\ 1 \end{matrix} \right]

齐次坐标除了方便用于进行仿射(线性)几何变换以后。它还能够能够用来明确区分向量和点。

向量v是矢量,它没有平移的概念,通过齐次坐标的N+1维 = 0,使得它无法完成平移操作,但是仍然可以受到RS Matrix影响。
\left[ \begin{matrix} x \\ y \\ 0 \end{matrix} \right] = \left[ \begin{matrix} 1 & 0 & T_x \\ 0 & 1 & T_y \\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} x \\ y \\ 0 \end{matrix} \right]

点P的齐次坐标的N+1维 = 1, 这就回答了问题2。

1.3 矩阵运算的左乘和右乘

由于矩阵运算满足如下规律:

我们以平移矩阵为例,根据上述公式可以改写成如下形式,这就是矩阵左乘:
\left[ \begin{matrix} x' & y' & 1 \end{matrix} \right ] = \left[ \begin{matrix} x & y & 1 \end{matrix} \right] * \left[ \begin{matrix} 1 & 0 & 0 \\ 0 & 1 & 0\\ T_x & T_y & 1 \end{matrix} \right ]

我们把向量在左边的矩阵乘法称之为:矩阵左乘**
我们把向量在右边的矩阵乘法称之为:矩阵右乘**

ps:unity就是基于矩阵右乘的,我们可以通过简单的创建translate matrix来查看translate系数的所在位置来推测unity的矩阵是否右乘

     Matrix4x4 m  = Matrix4x4.Translate(new Vector3(5, 6, 7));

左乘和右乘在计算效率上有深入的考量,同时左乘右乘影响着rts矩阵的运算顺序,详情请看下文

1.4 矩阵的存储方式,行主序&列主序

针对一个特定的矩阵,它在内存中的线性存储的方式有两种:行主序 & 列主序

对于一个数组float[9] array
行主序的存储方式是:a - b - c - d - e - f - g - h - i
列主序的存储方式是:a - d - g - b - e - h - c - f - i

ps:unity的matrix4x4就是列主序的,请注意m.xy中x是行位置,y是列位置,他们和行主序列主序无关

2. 坐标系转换

上文讲述了在basic coordinate下针对一个点P的Rts Formula,计算结果一直都在同一个坐标系下。

现在我们考虑一个新的问题:

在一个给定的basic coordinate(左手坐标系or右手坐标系)下有一个点P,计算新的坐标系A下的点P'的值

举个例子,我们提供两个坐标系X''_O'_Y''和X_O_Y,如何计算在X''_O'_Y''坐标系下的P''点在X'_O'_Y'中的值呢?

coordinate_world_matrix.png

复杂的问题简单化,我们先计算X''_O'_Y''中的P''在X'_O'_Y'中的P'值:

然后,我们在X'_O'_Y'中的点P'计算 X_O_Y的最后结果P

把上述公式通过矩阵左乘的方式表达:

\left[ \begin{matrix} x & y & 1 \end{matrix} \right] = \left[ \begin{matrix} x'' & y'' & 1 \end{matrix} \right] * \left[ \begin{matrix} cosθ & -sinθ & 0 \\ sinθ & cosθ & 0\\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} 1 & 0 & 0 \\ 0 & 1 & 0\\ a & b & 1 \end{matrix} \right] = \left[ \begin{matrix} cosθ & -sinθ & 0 \\ sinθ & cosθ & 0\\ a & b & 1 \end{matrix} \right]

通过矩阵的转置计算公式,我们可以得到矩阵右乘的版本:

\left[ \begin{matrix} x\\ y \\ 1 \end{matrix} \right] = \left[ \begin{matrix} 1 & 0 & a \\ 0 & 1 & b \\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} cosθ & sinθ & 0 \\ -sinθ & cosθ & 0 \\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} x''\\ y'' \\ 1 \end{matrix} \right] = \left[ \begin{matrix} cosθ & sinθ & a \\ -sinθ & cosθ & b \\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} x''\\ y'' \\ 1 \end{matrix} \right]

通过上述推导结果,我们能够观察到一些坐标系变换必须要注意的性质:

  1. 矩阵的左乘&右乘直接影响了坐标系变化下的rotate translate顺序。设X_O_Y是basic coordinate,X''O''Y''是local coordinate,那么上述公式就成了local 2 world coordinate formula,通过矩阵右乘,局部坐标系P''变换到P需要先乘Mt,再乘Mr

从P_local变换到P_world坐标系下,可以分成三个步骤:

  • X''_O'_Y'' 旋转到与X'O''Y'重合 -- 计算P''在X'O''Y'中的值P'
  • X'_O'_Y' 缩放到与X_O_Y一致
  • X'_O'_Y' 平移到与X_O_Y重合 -- 计算P'在X_O_Y的值P
  1. unity中game object的transform.rotation是basic coordinate下旋转θ到transform local coordinate的旋转四元数。
vector3 p' = Matrix4x4.rotate(transform.rotation).multiPoint(p)

注意:如果p是basic coordinate下, p'仍然是basic coordinate下的,上述这样使用旋转四元数并不会让点实现坐标系变换

由于在左手坐标系下,世界坐标旋转θ的公式已知

将上述公式带入坐标系变换公式,就可以得到

所以给定一个transform和基于这个transform的local coordinate点P,计算这个P在世界坐标系的代码为:

  protected Vector3 Coordinate2World(Transform a, Vector3 a_local_p)
  {
    // transform.rotation equals FromToRotation(Vector3.forward, a.forward)
    //Quaternion q = Quaternion.FromToRotation(Vector3.forward, a.forward);
    //Matrix4x4 m_q = Matrix4x4.Rotate(q);

    Matrix4x4 m_q = Matrix4x4.Rotate(a.rotation);
    Matrix4x4 m_t = Matrix4x4.Translate(a.position);

    return (m_t * m_q).MultiplyPoint(a_local_p);
  }

这个旋转应用于一些特殊的向量就会有特殊的几何意义,比如vector.forward使用下面的代码

vector3 v' = Matrix4x4.rotate(transform.rotation).multiVector(v)

得到的V'就是local coordinate下vector.forward在basic coordinate中的值

  1. 矩阵右乘的local coordinate <--> world coordinate matrix & 中,矩阵的相关位置对应着相应的功能,旋转&缩放相关的参数为R,平移相关的参数为T

矩阵右乘 --- 在local to world matrix中,T = obj.position - vector3.zero = obj.position = vec2(m02, m12)
矩阵右乘 --- 在world to local matrix中,T = -obj.position + vector3.zero = -obj.position = vec2(m02, m12)

小提示:由于unity是使用矩阵右乘的,我们在shader中可以很方便的在unity_ObjectToWorld获取物体的world position。

float4 worldCoord = float4(unity_ObjectToWorld._m03, unity_ObjectToWorld._m13, unity_ObjectToWorld._m23, 1);
  1. 设X_O_Y和X''_O'_Y''都是local coordinate,这样我们就得到了一个广义的旋转矩阵推导,同时回答了第二章一开始提出的问题

在一个给定的坐标系A(左手坐标系or右手坐标系)下有一个点P,计算新的坐标系B下的点P'的值

从上文可以得到坐标系变换的头一步需要将 X''_O'_Y'' 的P''点变换到一个虚拟的坐标系X'O''Y'下,这一步需要X'O''Y'下旋转角度θ到X''_O'_Y'' 的矩阵translate(O''_inworld - O'_inworld),计算代码如下:

  protected Vector3 Coordinate2Coordinate(Transform a, Vector3 a_local_p, Transform b)
  {
    Quaternion q = Quaternion.FromToRotation(b.forward, a.forward);

    Matrix4x4 m_q = Matrix4x4.Rotate(q);
    Matrix4x4 m_t = Matrix4x4.Translate(a.position - b.position);

    return (m_t * m_q).MultiplyPoint(a_local_p);
  }

3. 总结

上文中的公式推导都是基于二维坐标系的,但是RTS的顺序,坐标系变换原理都是一样的,所有的rts变换在计算目标不同的时候公式是不同的。

在baisc coordinate下对P点进行旋转,平移,缩放得到的结果P'仍然是在basic coordinate中,而basic coordinate下有点P,获取local coordinate的坐标P'是另外一回事,不要搞混了。

下一篇文章会仔细推导一下三维坐标系的rts矩阵,和上述两种情况下的计算公式。

所有资源来自互联网,如有侵权,烦请告知。纰漏之处,请多多指教

东南形胜,三吴都会,钱塘自古繁华,

烟柳画桥,风帘翠幕,参差十万人家。

2020/3/21 北京 望京soho 赴杭前夕

你可能感兴趣的:(特效笔记 -- 搞定坐标系变换_左乘_右乘_行主序_列主序的倒数第二篇文章)