前言
筆者是一個(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)系。
定義左右手坐標(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)系則正好相反
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è)問題:
- 為什么要用三維向量去表示二維世界的P&V
- 為什么P的第三維度值是1,而V的第三維度值是0
1.1 二維坐標(biāo)系的Rotate Translate Scale Formula
要回答這些問題我們需要考慮這樣的問題,在二維世界中如何對點(diǎn)P完成平移(translate)旋轉(zhuǎn)(rotate)以及縮放(scale)操作。
考慮下圖中的點(diǎn)P移動到P',有公式如下:
考慮下圖中的點(diǎn)P旋轉(zhuǎn)到P'-- 在左手坐標(biāo)系下旋轉(zhuǎn)角度θ的計(jì)算公式
接下來我們考慮縮放的情況,對二維坐標(biāo)系內(nèi)上X,Y軸分別進(jìn)行放縮Sx,Sy,計(jì)算公式如下:
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
Scale Matirx in left hand coordinate
在n維坐標(biāo)系中,平移矩陣需要n+1維的向量完成平移操作。所謂的齊次坐標(biāo)就是就是將一個(gè)原本是n維的向量用一個(gè)n+1維向量來表示。
Translate Matirx in left hand coordinate
注意:以上的計(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
Scale Matirx in left hand coordinate
齊次坐標(biāo)除了方便用于進(jìn)行仿射(線性)幾何變換以后。它還能夠能夠用來明確區(qū)分向量和點(diǎn)。
向量v是矢量,它沒有平移的概念,通過齊次坐標(biāo)的N+1維 = 0,使得它無法完成平移操作,但是仍然可以受到RS Matrix影響。
點(diǎn)P的齊次坐標(biāo)的N+1維 = 1, 這就回答了問題2。
1.3 矩陣運(yùn)算的左乘和右乘
由于矩陣運(yùn)算滿足如下規(guī)律:
我們以平移矩陣為例,根據(jù)上述公式可以改寫成如下形式,這就是矩陣左乘:
我們把向量在左邊的矩陣乘法稱之為:矩陣左乘**
我們把向量在右邊的矩陣乘法稱之為:矩陣右乘**
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)存中的線性存儲的方式有兩種:行主序 & 列主序
對于一個(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'中的值呢?
復(fù)雜的問題簡單化,我們先計(jì)算X''_O'_Y''中的P''在X'_O'_Y'中的P'值:
然后,我們在X'_O'_Y'中的點(diǎn)P'計(jì)算 X_O_Y的最后結(jié)果P
把上述公式通過矩陣左乘的方式表達(dá):
通過矩陣的轉(zhuǎn)置計(jì)算公式,我們可以得到矩陣右乘的版本:
通過上述推導(dǎo)結(jié)果,我們能夠觀察到一些坐標(biāo)系變換必須要注意的性質(zhì):
- 矩陣的左乘&右乘直接影響了坐標(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_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
- 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)θ的公式已知
將上述公式帶入坐標(biāo)系變換公式,就可以得到
所以給定一個(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中的值
- 矩陣右乘的local coordinate <--> world coordinate matrix & 中,矩陣的相關(guān)位置對應(yīng)著相應(yīng)的功能,旋轉(zhuǎn)&縮放相關(guān)的參數(shù)為R,平移相關(guān)的參數(shù)為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);
- 設(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ì)算公式。