美文网首页
特效笔记 -- 搞定坐标系变换_左乘_右乘_行主序_列主序的倒数

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

作者: shared_ptr | 来源:发表于2020-03-22 11:31 被阅读0次

    前言

    笔者是一个游戏行业的程序员,读书的时候做的基本都是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

    x' = x + t_x y' = y + t_y

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

    rotation.png

    x = R * cosΦ y = R * sinΦ

    x' = R * cos(Φ-θ) = R * cosθ * cosΦ + R * sinθ * sinΦ=x*cosθ + y * sinθ y' = R * sin(Φ-θ) = R * cosθ * sinΦ - R * sinθ * cosΦ=-x*sinθ + y * cosθ

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

    1.2 二维坐标系的Rotate Translate Scale Matrix

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

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

    Rotate Matirx in left hand coordinate
    \left[ \begin{matrix} x'\\ y' \end{matrix} \right ] = \left[ \begin{matrix} cosθ & sinθ \\ -sinθ & cosθ \end{matrix} \right] * \left[ \begin{matrix} x\\ y \end{matrix} \right]

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

    在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 矩阵运算的左乘和右乘

    由于矩阵运算满足如下规律:
    (A * B)^T=B^T*A^T

    我们以平移矩阵为例,根据上述公式可以改写成如下形式,这就是矩阵左乘:
    \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 矩阵的存储方式,行主序&列主序

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

    \left[ \begin{matrix} a & b & c \\ d & e & f\\ g & h & i \end{matrix} \right]

    对于一个数组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'' = R * cosθ_3 y'' = R * sinθ_3

    x' = R * cos(θ_3-θ_2) = R * cosθ_3 * cosθ_2 + R * sinθ_3 * sinθ_2=x''*cosθ_2 + y'' * sinθ_2 y' = R * sin(θ_3-θ_2) = R * sinθ_3 * cosθ_2 - R * cosθ_3 * sinθ_2=-x''*sinθ_2 + y'' * cosθ_2

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

    x = x' + a y = y' +b

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

    \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_w = M_t * M_r * P_l P_l = M^{-1}_r * M^{-1}_t * P_w

    从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下的,上述这样使用旋转四元数并不会让点实现坐标系变换

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

    \left[ \begin{matrix} cosθ & sinθ & 0 \\ -sinθ & cosθ & 0 \\ 0 & 0 & 1 \end{matrix} \right] = transform.rotation

    将上述公式带入坐标系变换公式,就可以得到
    \left[ \begin{matrix} x\\ y \\ 1 \end{matrix} \right] = transform.position * transform.rotation * \left[ \begin{matrix} x''\\ y'' \\ 1 \end{matrix} \right]

    所以给定一个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

    \left[ \begin{matrix} R & R & T \\ R & R & T \\ 0 & 0 & 1 \end{matrix} \right] * \left[ \begin{matrix} x\\ y \\ 1 \end{matrix} \right ]

    矩阵右乘 --- 在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 赴杭前夕

    相关文章

      网友评论

          本文标题:特效笔记 -- 搞定坐标系变换_左乘_右乘_行主序_列主序的倒数

          本文链接:https://www.haomeiwen.com/subject/uxiuyhtx.html