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

前言

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

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

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

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

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

left_hand_坐標(biāo)系.jpg

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


在三維世界中,我們給定一個(gè)平面XOY,針對這個(gè)平面的旋轉(zhuǎn)角度θ就產(chǎn)生兩種情況--順時(shí)針和逆時(shí)針。所以對坐標(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)系的正方向進(jìn)行旋轉(zhuǎn)θ角度。

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

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

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

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

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

為了理解簡單點(diǎn),本文大部分都會先考慮二維的情況,三維世界的情況其實(shí)就是二維世界的擴(kuò)展

在二維世界中,點(diǎn)P&向量V的定義:

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

這里你可能會提出兩個(gè)問題:

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

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

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

考慮下圖中的點(diǎn)P移動到P',有公式如下:

translation.jpg

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

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

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軸分別進(jìn)行放縮Sx,Sy,計(jì)算公式如下:
x' = x * S_x y' = y * S_y

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

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

根據(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)就是就是將一個(gè)原本是n維的向量用一個(gè)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 ]

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

進(jìn)而我們把旋轉(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)除了方便用于進(jìn)行仿射(線性)幾何變換以后。它還能夠能夠用來明確區(qū)分向量和點(diǎn)。

向量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]

點(diǎn)P的齊次坐標(biāo)的N+1維 = 1, 這就回答了問題2。

1.3 矩陣運(yùn)算的左乘和右乘

由于矩陣運(yùn)算滿足如下規(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));

左乘和右乘在計(jì)算效率上有深入的考量,同時(shí)左乘右乘影響著rts矩陣的運(yùn)算順序,詳情請看下文

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

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

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

對于一個(gè)數(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下針對一個(gè)點(diǎn)P的Rts Formula,計(jì)算結(jié)果一直都在同一個(gè)坐標(biāo)系下。

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

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

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

coordinate_world_matrix.png

復(fù)雜的問題簡單化,我們先計(jì)算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'中的點(diǎn)P'計(jì)算 X_O_Y的最后結(jié)果P

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

把上述公式通過矩陣左乘的方式表達(dá):

\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)置計(jì)算公式,我們可以得到矩陣右乘的版本:

\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)系下,可以分成三個(gè)步驟:

  • X''_O'_Y'' 旋轉(zhuǎn)到與X'O''Y'重合 -- 計(jì)算P''在X'O''Y'中的值P'
  • X'_O'_Y' 縮放到與X_O_Y一致
  • X'_O'_Y' 平移到與X_O_Y重合 -- 計(jì)算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ù)并不會讓點(diǎ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]

所以給定一個(gè)transform和基于這個(gè)transform的local coordinate點(diǎn)P,計(jì)算這個(gè)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);
  }

這個(gè)旋轉(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,這樣我們就得到了一個(gè)廣義的旋轉(zhuǎn)矩陣推導(dǎo),同時(shí)回答了第二章一開始提出的問題

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

從上文可以得到坐標(biāo)系變換的頭一步需要將 X''_O'_Y'' 的P''點(diǎn)變換到一個(gè)虛擬的坐標(biāo)系X'O''Y'下,這一步需要X'O''Y'下旋轉(zhuǎn)角度θ到X''_O'_Y'' 的矩陣translate(O''_inworld - O'_inworld),計(jì)算代碼如下:

  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變換在計(jì)算目標(biāo)不同的時(shí)候公式是不同的。

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

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

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

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

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

2020/3/21 北京 望京soho 赴杭前夕
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,737評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評論 2 370