特效筆記 -- 搞定坐標(biāo)系變換_左乘_右乘_行主序_列主序的倒數(shù)第二篇文章

前言

筆者是一個游戲行業(yè)的程序員,讀書的時候做的基本都是native graphic library的項目,工作了反而對渲染管線細節(jié)接觸少很多,說到底還是現(xiàn)在的商業(yè)引擎都太好用了啊喂。目前絕大多數(shù)的渲染算法在github,shader toy上都能找到非常完整的實現(xiàn),大量的內(nèi)置函數(shù)和宏隱藏了復(fù)雜的渲染細節(jié)。假如你需要修改unity自帶的復(fù)雜pbr光照,或者實現(xiàn)一個簡單的billboard shader,做為程序員為了能繼續(xù)方便的打磨別人的輪子,搞明白這些內(nèi)置函數(shù)&變量的數(shù)據(jù)計算過程就成了必要條件。

這篇文章包含了左&右手坐標(biāo)系 行主序(row major) 列主序(column major)左乘 右乘 坐標(biāo)系變換的相關(guān)推導(dǎo)和使用注意事項。

相信我,我高考數(shù)學(xué)選擇錯了一半都能搞懂,你肯定也可以。

左手坐標(biāo)系&右手坐標(biāo)系

左手坐標(biāo)系是指在空間直角坐標(biāo)系中,讓左手拇指指向x軸的正方向,食指指向y軸的正方向,如果中指能指向z軸的正方向,則稱這個坐標(biāo)系為左手直角坐標(biāo)系。反之則是右手直角坐標(biāo)系。

left_hand_坐標(biāo)系.jpg

定義左右手坐標(biāo)系的作用:


在三維世界中,我們給定一個平面XOY,針對這個平面的旋轉(zhuǎn)角度θ就產(chǎn)生兩種情況--順時針和逆時針。所以對坐標(biāo)系使用左手與右手的命名,這種命名規(guī)則的作用就是用來方便判斷旋轉(zhuǎn)的正方向,這就是左手法則和右手法則。

針對上圖來說,在左手坐標(biāo)系下,XOY面的旋轉(zhuǎn)正方向這樣獲得:大拇指朝向z軸正方向,四指彎曲的方向就是左手坐標(biāo)系下,旋轉(zhuǎn)的正方向。所以本文的所有旋轉(zhuǎn)在給定左右手坐標(biāo)系的情況下,旋轉(zhuǎn)角度θ,就是沿著當(dāng)前坐標(biāo)系的正方向進行旋轉(zhuǎn)θ角度。

測試可知:左手坐標(biāo)下,順時針就是旋轉(zhuǎn)的正方向,右手坐標(biāo)系則正好相反

left_hand_坐標(biāo)系_標(biāo)記.jpg

ps:unity就是基于左手坐標(biāo)系的,我們可以通過簡單的代碼進行觀察物體是否沿著XOZ面順時針旋轉(zhuǎn)

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

1. 點,向量,齊次坐標(biāo)

為了理解簡單點,本文大部分都會先考慮二維的情況,三維世界的情況其實就是二維世界的擴展

在二維世界中,點P&向量V的定義:

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

這里你可能會提出兩個問題:

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

1.1 二維坐標(biāo)系的Rotate Translate Scale Formula

要回答這些問題我們需要考慮這樣的問題,在二維世界中如何對點P完成平移(translate)旋轉(zhuǎn)(rotate)以及縮放(scale)操作。

考慮下圖中的點P移動到P',有公式如下:

translation.jpg

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

考慮下圖中的點P旋轉(zhuǎn)到P'-- 在左手坐標(biāo)系下旋轉(zhuǎn)角度θ的計算公式

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θ

接下來我們考慮縮放的情況,對二維坐標(biāo)系內(nèi)上X,Y軸分別進行放縮Sx,Sy,計算公式如下:
x' = x * S_x y' = y * S_y

1.2 二維坐標(biāo)系的Rotate Translate Scale Matrix

我們接下來考慮一個問題,如何方便的把RTS運算結(jié)合在一起呢?答案就是矩陣。原因很簡單,矩陣運算天生滿足結(jié)合律,我們把上述公式轉(zhuǎn)換為矩陣運算后就可以用一個矩陣來表示RTS行為了,這為以后的復(fù)雜運算提供了便利。

根據(jù)上述的公式,我們可以很簡單的得到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維坐標(biāo)系中,平移矩陣需要n+1維的向量完成平移操作。所謂的齊次坐標(biāo)就是就是將一個原本是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 ]

注意:以上的計算公式計算結(jié)果都是在同一個坐標(biāo)系內(nèi)部的,當(dāng)我們使用XOY為basic coordinate的情況下,以上公式的計算結(jié)果都是在basic coordinate下的

進而我們把旋轉(zhuǎn)矩陣和平移矩陣也引入齊次坐標(biāo)來使得Rts Matrix可以結(jié)合,公式為:
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]

齊次坐標(biāo)除了方便用于進行仿射(線性)幾何變換以后。它還能夠能夠用來明確區(qū)分向量和點。

向量v是矢量,它沒有平移的概念,通過齊次坐標(biāo)的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的齊次坐標(biāo)的N+1維 = 1, 這就回答了問題2。

1.3 矩陣運算的左乘和右乘

由于矩陣運算滿足如下規(guī)律:
(A * B)^T=B^T*A^T

我們以平移矩陣為例,根據(jù)上述公式可以改寫成如下形式,這就是矩陣左乘:
\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就是基于矩陣右乘的,我們可以通過簡單的創(chuàng)建translate matrix來查看translate系數(shù)的所在位置來推測unity的矩陣是否右乘

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

左乘和右乘在計算效率上有深入的考量,同時左乘右乘影響著rts矩陣的運算順序,詳情請看下文

1.4 矩陣的存儲方式,行主序&列主序

針對一個特定的矩陣,它在內(nèi)存中的線性存儲的方式有兩種:行主序 & 列主序

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

對于一個數(shù)組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是列位置,他們和行主序列主序無關(guān)

2. 坐標(biāo)系轉(zhuǎn)換

上文講述了在basic coordinate下針對一個點P的Rts Formula,計算結(jié)果一直都在同一個坐標(biāo)系下。

現(xiàn)在我們考慮一個新的問題:

在一個給定的basic coordinate(左手坐標(biāo)系or右手坐標(biāo)系)下有一個點P,計算新的坐標(biāo)系A(chǔ)下的點P'的值

舉個例子,我們提供兩個坐標(biāo)系X''_O'_Y''和X_O_Y,如何計算在X''_O'_Y''坐標(biāo)系下的P''點在X'_O'_Y'中的值呢?

coordinate_world_matrix.png

復(fù)雜的問題簡單化,我們先計算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的最后結(jié)果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]

通過矩陣的轉(zhuǎn)置計算公式,我們可以得到矩陣右乘的版本:

\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]

通過上述推導(dǎo)結(jié)果,我們能夠觀察到一些坐標(biāo)系變換必須要注意的性質(zhì):

  1. 矩陣的左乘&右乘直接影響了坐標(biāo)系變化下的rotate translate順序。設(shè)X_O_Y是basic coordinate,X''O''Y''是local coordinate,那么上述公式就成了local 2 world coordinate formula,通過矩陣右乘,局部坐標(biāo)系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坐標(biāo)系下,可以分成三個步驟:

  • X''_O'_Y'' 旋轉(zhuǎn)到與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中g(shù)ame object的transform.rotation是basic coordinate下旋轉(zhuǎn)θ到transform local coordinate的旋轉(zhuǎn)四元數(shù)。
vector3 p' = Matrix4x4.rotate(transform.rotation).multiPoint(p)

注意:如果p是basic coordinate下, p'仍然是basic coordinate下的,上述這樣使用旋轉(zhuǎn)四元數(shù)并不會讓點實現(xiàn)坐標(biāo)系變換

由于在左手坐標(biāo)系下,世界坐標(biāo)旋轉(zhuǎn)θ的公式已知

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

將上述公式帶入坐標(biāo)系變換公式,就可以得到
\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在世界坐標(biāo)系的代碼為:

  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);
  }

這個旋轉(zhuǎn)應(yīng)用于一些特殊的向量就會有特殊的幾何意義,比如vector.forward使用下面的代碼

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

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

  1. 矩陣右乘的local coordinate <--> world coordinate matrix & 中,矩陣的相關(guān)位置對應(yīng)著相應(yīng)的功能,旋轉(zhuǎn)&縮放相關(guān)的參數(shù)為R,平移相關(guān)的參數(shù)為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. 設(shè)X_O_Y和X''_O'_Y''都是local coordinate,這樣我們就得到了一個廣義的旋轉(zhuǎn)矩陣推導(dǎo),同時回答了第二章一開始提出的問題

在一個給定的坐標(biāo)系A(chǔ)(左手坐標(biāo)系or右手坐標(biāo)系)下有一個點P,計算新的坐標(biāo)系B下的點P'的值

從上文可以得到坐標(biāo)系變換的頭一步需要將 X''_O'_Y'' 的P''點變換到一個虛擬的坐標(biāo)系X'O''Y'下,這一步需要X'O''Y'下旋轉(zhuǎn)角度θ到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. 總結(jié)

上文中的公式推導(dǎo)都是基于二維坐標(biāo)系的,但是RTS的順序,坐標(biāo)系變換原理都是一樣的,所有的rts變換在計算目標(biāo)不同的時候公式是不同的。

在baisc coordinate下對P點進行旋轉(zhuǎn),平移,縮放得到的結(jié)果P'仍然是在basic coordinate中,而basic coordinate下有點P,獲取local coordinate的坐標(biāo)P'是另外一回事,不要搞混了。

下一篇文章會仔細推導(dǎo)一下三維坐標(biāo)系的rts矩陣,和上述兩種情況下的計算公式。

所有資源來自互聯(lián)網(wǎng),如有侵權(quán),煩請告知。紕漏之處,請多多指教

東南形勝,三吳都會,錢塘自古繁華,

煙柳畫橋,風(fēng)簾翠幕,參差十萬人家。

2020/3/21 北京 望京soho 赴杭前夕
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容