1、齊次座標git
在3D世界中表示一個點的方式是:(x, y, z);然而在3D世界中表示一個向量的方式也是:(x, y, z);若是咱們只給一個三元組(x, y, z)鬼知道這是向量仍是點,畢竟點與向量仍是有很大區別的,點只表示位置,向量沒有位置只有大小和方向。爲了區分點和向量咱們給它加上一維,用(x, y, z, w)這種四元組的方式來表達座標,咱們規定(x, y, z, 0)表示一個向量,(x, y, z, 1)或(x', y', z', 2)等w不爲0時來表示點。這種用n+1維座標表示n維座標的方式稱爲齊次座標。github
齊次座標除了可以區分點和向量,在3D圖形學中還有重要的意義。齊次座標系使得咱們能夠在一中特殊的方程組中求出解,這個方程組中每個方程都表示一個與系統中其餘直線平行的直線。咱們知道在歐幾里得空間中,對這種方程組是無解的,由於他們沒有交點。然而在現實世界中咱們是能夠看到兩條平行線相交的。web
兩條平行的鐵路最終相較於無窮遠處。這就說明人眼看到的世界並非歐幾里得空間,而是在一個名爲透視空間中的世界。因此要在2D屏幕上表示3D世界,咱們須要一個數學工具來承擔這項任務,而齊次座標很完美的承擔了這項任務。算法
若是咱們知道一個三維點的齊次座標爲(X, Y, Z, w),那麼它的3D空間座標爲:spring
x = X / w編程
y = Y / wcanvas
z = Z / wapi
咱們能夠看到齊次座標(1, 2, 3, 1)與(2, 4, 6, 2)表示的都是3d空間中的點(1, 2, 3);因此一般在程序設計中咱們都取w爲1.數組
如今咱們再來看一下上面說的齊次座標在一組平行線中求解,有兩條直線:編程語言
Ax + By + Cz + D = 0
Ax + By + Cz + d = 0;
D不等於d;根據解析幾何知識咱們能夠知道這是兩條在歐幾里得空間中這是兩條相交的平行線,它們不可能有交點。若是d = D兩條直線會重合。如今咱們把他們用齊次座標來表示:
A(X/w) + B(Y/w) + C(Z/w) + D = 0;
A (X / w) + B (Y/w) + C (Z/w) + d = 0;
方程組兩邊同時乘以w獲得:
AX + BY + CZ + Dw = 0;
AX + BY + CZ + dw = 0;
因此在齊次空間中對於四元組(X, Y, Z, w)(想一下極限的概念)當w無限趨近於0時,歐幾里得空間中的兩條平行線有無窮多個解(X, Y, Z, 0);他們再無窮遠處相交了。如同咱們人眼看到的現實世界中兩條平行線相交同樣。
2、矩陣迷宮
咱們先來看一下在2d中將一個點(x, y)繞原點旋轉 α 角度獲得(x', y')的過程:
對於點x,y的極座標表示爲:
x = r * cosβ
y = r * sinβ
旋轉後的座標x', y'爲:
x' = r * cos(α + β) = rcosβcosα- rsinαsinβ = xcosα - ysinα
y' = r * sin(α + β) = rcosαsinβ + rsinαcosβ = ycosα + xsinα
那麼咱們用n+1爲齊次座標並結合矩陣表示爲:
我在學習webgl過程當中常常有一個疑問,爲何矩陣能夠表示空間變換,有一個大牛告訴我,表示空間變換的並非矩陣自己,而是一系列數學公式,就像上面用到的三角函數公式同樣,而矩陣的運算法則可以指把公式的運算結果很好的表達出來。要想搞明白這些矩陣表示的空間變換須要本身手動的把這些變換結果推導出來。
另外有看過opengl相關矩陣運算的同窗必定會發現上文中的運算用的是行向量的形式,而opengl用的是列向量形式,從行向量到列向量只須要轉支一下便可。這裏也正是我想重點強調的,對於初學者來講,行向量/列向量、行存儲/列存儲、以及平移旋轉的表達順序,這三者糅雜在一塊兒很容易把人繞暈,由於它有2*2*2=8中狀況。尤爲是不一樣的書籍他們使用的表達方式、存儲方式、以及對運算順序的表達是徹底相反的(好比《3D數學基礎》跟《opengl權威指南》就是徹底相反的),爲了統一塊兒見,我建議你們按照這樣的方式來思考問題:
1)webgl中使用的是列向量,對應的縮放、平移、旋轉矩陣爲:
2)webgl使用的是列存儲
在實際編程語言中,咱們使用的一維數組來存儲4x4矩陣的16個元素。所謂的行存儲和列存儲的區分就在於數組的前四個元素存儲的是矩陣的第一列仍是第一行;表示列的稱爲列存儲,表示行的成爲行存儲。以下圖數組的前四個元素對應矩陣中的第一列,因此是列存儲。
3)webgl中矩陣的運算順序是從右往左進行的
當矩陣相乘時,在最右邊的矩陣是第一個與向量相乘的,因此你應該從右向左讀這個乘法。因此先旋轉後平移的的矩陣操做是TRV而不是RTV。我發如今跟同事討論問題時,每每就是你們在這個地方有分歧而說到兩個不一樣的方向最後吵起來。由於有的同事看到TRV他按照左往右讀的說法是先平移後旋轉,而後你們就在先平移後旋轉仍是先旋轉後平移裏爭執。(跟人討論矩陣運算順序時必定要寫在紙上)
矩陣乘法是不遵照交換律的,這意味着它們的順序很重要。建議您在組合矩陣時,先進行縮放操做,而後是旋轉,最後纔是位移,不然它們會(消極地)互相影響。好比,若是你先位移再縮放,位移的向量也會一樣被縮放(好比向某方向移動2米,2米也許會被縮放成1米)!
肯定矩陣運算順序後,接下來要肯定矩陣操做類庫的api的調用順序。向glmatrix這種類庫提供的api,對先旋轉後平移這種矩陣操做的實現方式是:(看api的很容易讓人認爲是先平移後旋轉)
因此在webgl中通常api的調用順序都是跟矩陣的運算順序相反的,這點與opengl一致。
另外注意一下:有不少書籍會告訴你一個矩陣的某些元素表明旋轉,某幾個元素表明平移,好比左上角9個元素表明旋轉,12-15表明平移,實際這些都不必定,一旦矩陣有了組合操做,那麼這些均可能改變。
3、模型矩陣與模型視圖矩陣
現實世界中咱們能夠創建各類座標系,若是咱們以一個物體原點(本身任意指定)來創建座標系,而且這個座標系在初始時與世界座標系重合,那麼這個物體上的全部點的座標都是相對這個局部座標系來。若是咱們移動或者旋轉縮放物體,咱們會使用一個矩陣來編碼這些變換。這個矩陣稱爲模型矩陣。在咱們用模型矩陣乘以咱們對象中的頂點就獲得一系列新的座標,這些座標就是物體在世界座標系中的頂點位置。
咱們在2D屏幕上顯示三維物體,就像用相機拍攝圖像同樣。在三維世界中有一個假想的相機,咱們在屏幕上看到的場景都是在相機座標系下表示的,要把世界座標系中的點轉化成相機座標系的點,咱們就須要一個變換矩陣。這個矩陣稱爲模型視圖矩陣,而模型視圖矩陣就是相機的模型矩陣的逆矩陣。
咱們想要看到世界中的任何場景只要控制相機的移動和旋轉便可。用戶控制相機的過程主要是兩個事情:朝向和位置。只要這兩個屬性肯定了,相機的模型矩陣以及模型視圖矩陣均可以獲得了。用3D變換的角度來講就是旋轉和平移。能夠想象對於任意一個3d場景咱們均可以將相機作一個旋轉而後平移到一個位置來觀察到它的任意細節。
如今咱們先改變相機的朝向而後平移到一個位置,這個模型矩陣爲:
C = TR
T表明平移變換,R表明旋轉變換(R的前三列表明相機旋轉後的三個座標軸),那麼這時候的模型視圖矩陣爲:
這個C-1就是咱們要的模型視圖矩陣,上面說到相機旋轉後的三個軸是互相垂直的,也就是正交的,而正交矩陣的逆矩陣等於矩陣的轉置矩陣。因此C-1最終變爲:
而T的逆矩陣很簡單:
最終的模型視圖矩陣爲:
而咱們在三維開發中經常使用的求模型視圖矩陣的方法lookAt用的就是這個原理。
這個函數主要須要三個參數:eye表明相機位置、target表明相機的目標點、up表明相機的上方向。咱們稱相機模型矩陣的第一列表明相機的x軸,咱們稱爲right向量;第二列表明相機的y軸,咱們稱爲up向量,第三列表明相機的z軸,咱們稱爲相機軸(相機軸並非相機的朝向,而是相機朝向的負方向,另外這裏咱們的相機的模型矩陣統一使用的右手系,有的資料裏面用的是左手系)。
mat4.lookAt = function (eye, center, up, dest) { if (!dest) { dest = mat4.create(); } var x0, x1, x2, y0, y1, y2, z0, z1, z2, len, eyex = eye[0], eyey = eye[1], eyez = eye[2], upx = up[0], upy = up[1], upz = up[2], centerx = center[0], centery = center[1], centerz = center[2]; if (eyex === centerx && eyey === centery && eyez === centerz) { return mat4.identity(dest); } //vec3.direction(eye, center, z); // 首先根據觀察點和相機位置求得相機軸向量 z0 = eyex - centerx; z1 = eyey - centery; z2 = eyez - centerz; // normalize (no check needed for 0 because of early return) // 對相機軸作標準化 len = 1 / Math.sqrt(z0 \* z0 + z1 \* z1 + z2 \* z2); z0 \*= len; z1 \*= len; z2 \*= len; //vec3.normalize(vec3.cross(up, z, x)); // up向量叉乘z軸獲得x軸,即咱們說的right向量 x0 = upy \* z2 - upz \* z1; x1 = upz \* z0 - upx \* z2; x2 = upx \* z1 - upy \* z0; len = Math.sqrt(x0 \* x0 + x1 \* x1 + x2 \* x2); if (!len) { x0 = 0; x1 = 0; x2 = 0; } else { len = 1 / len; x0 \*= len; x1 \*= len; x2 \*= len; } //vec3.normalize(vec3.cross(z, x, y)); // 而後根據z軸叉乘x軸獲得相機的y軸 y0 = z1 \* x2 - z2 \* x1; y1 = z2 \* x0 - z0 \* x2; y2 = z0 \* x1 - z1 \* x0; len = Math.sqrt(y0 \* y0 + y1 \* y1 + y2 \* y2); if (!len) { y0 = 0; y1 = 0; y2 = 0; } else { len = 1 / len; y0 \*= len; y1 \*= len; y2 \*= len; } // 最終獲得的模型視圖矩陣爲:R^T \* T^-1 dest[0] = x0; dest[1] = y0; dest[2] = z0; dest[3] = 0; dest[4] = x1; dest[5] = y1; dest[6] = z1; dest[7] = 0; dest[8] = x2; dest[9] = y2; dest[10] = z2; dest[11] = 0; dest[12] = -(x0 \* eyex + x1 \* eyey + x2 \* eyez); // -x軸點乘eye向量 dest[13] = -(y0 \* eyex + y1 \* eyey + y2 \* eyez); // -y軸點乘eye向量 dest[14] = -(z0 \* eyex + z1 \* eyey + z2 \* eyez); // -z軸點乘eye向量 dest[15] = 1; return dest; };
這裏講的都是經過先改變相機朝向而後改變相機位置的方式來觀察三維場景中的物體,實際上也經過別的方式好比先將相機平移到一個位置,而後繞世界座標系旋轉的方式來觀察場景,這種算法的效果就像是將相機固定在軌道上同樣(咱們經過先改變朝向在平移也能達到這種效果),在有的資料中它把這種先平移後旋轉方式稱爲軌道相機,把先旋轉後平移稱爲跟蹤相機。
4、透視矩陣
經過模型視圖變換,3d場景中的物體已經可以用相機空間座標來表達,接下來咱們處理的是如何來模擬人眼的近大遠小效果。相機座標系中的物體仍是處於3d世界中,要作出近大遠小的效果還須要繼續變換。這個變換被稱爲透視投影,它的特色是全部投影線都從空間一點投射,離視點近的物體投影大,離視點小的物體投影小,小到極點稱爲滅點。
通常將屏幕放在觀察者和物體之間。投影線與屏幕的焦點就是物體點上的透視投影。這裏咱們的觀察點就是相機的位置。
你們對透視投影有了基本認識,如今咱們來講一些透視除法也叫視錐體裁切,什麼意思呢?你們想一下咱們人眼是否是隻能看到一部分的世界內容,而不是所有,咱們視野範圍以外的內容已經被過濾掉了,因此在3d圖形學模擬人眼的過程當中也有一步就是將多餘內容裁切掉。
在3d圖形學中咱們模擬透視投影是經過一個六面體構造出投影矩陣來作透視效果:
除了穿過投影面正中心的投影線沒有變形外,與其它投影線相交的點都存在變形。假設點p在相機座標系下爲(x, y, z),對應着投影面上的點爲p'(x', y', z').
在相機座標系下,一個點在投影線上的投影對應爲它的z份量,那麼根據三角形類似法則,咱們就能求出對應的x'和y'與z的關係:
n/|z| = y' / y
y' = n*y / |z|
由於這裏在相機座標系下,z是負數因此|z| = -z,那麼上式變爲:y' = n * y / (-z);
同理可求出x爲:x' = n * x / (-z);
那麼咱們獲得的投影再近平面的座標p'爲(nx / (-z), ny / (-z), -n);
這個時候咱們發現p'的z份量永遠都是-n,也就是說原來p的z在投影后已經丟失了。
讓咱們先放一下,接下來咱們說一下透視除法,透視除法的目的是把投影變換後xyz任意一個不在-w與w之間的物體去除掉。然而在視錐體這裏面作裁切並不容易,因此數學前輩想了一個方式,讓咱們用一個立方體來作裁切,webgl中通過透視投影變換後的物體通過透視除法後會在一個xyz都是-1到1的立方體之間,這時候的座標稱爲設備歸一化座標,簡稱ndc。
那麼既然投影到近平面那部分座標的z值已經丟了,反正後面也要變換到ndc,乾脆這裏直接用一種方式來表示歸一化後的z;這裏咱們這樣設置近平面座標p':
p' = (-nx/z, -ny/z, (az+b) / z);
能夠看到x', y' 都與1/z成線性關係,因此這裏讓z'也與1/z保持線性關係,因此:Z(ndc) = b / z + a;
那麼將p'變爲齊次座標後爲:
(nx, ny, -az - b, -z);
這時候齊次座標來源於,投影矩陣x視座標 = p';
m * [x, y, z, 1]T = [nx, ny, -az - b, -z]T
可的矩陣m爲:
前面已經說到用(az + b)/z直接對應ndc座標,因此
當z = -n時,(az + b)/z = -1;
當z = -f時, (az + b)/z = 1;
聯立方程組得:
a = (n+f)/(f-n)
b = (2nf)/(f-n)
如今的矩陣m爲:
如今Zndc已經知足了-1到1的結果;咱們的x', y' 還停留在投影平面座標中,還須要由投影面轉到ndc座標中,數學家在處理兩者的關係時,選擇了以下的對應關係:
(-nx/z - left)/(right - left) = X(ndc) - (-1) / (1 - (-1))獲得
X(ndc) = -(2nx/z)/ (right - left) - (right+left)/(right-left);
(-ny/z - bottom) / (top - bottom) = Y(ndc) - (-1) / (1 - (-1))獲得
Y(ndc) = - (2ny/z) / (top - bottom) - (top + bottom) / (top - bottom);
那麼如今P(ndc) = [-(2nx/z)/ (right - left) - (right+left)/(right-left), - (2ny/z) / (top - bottom) - (top + bottom) / (top - bottom), (az+b)/z, 1]
齊次座標爲:
[2nx/(right-left) + (right+left)z/(right - left), 2ny/(top - bottom)+(top+bottom)z/(top-bottom), -az-b, -z];
而齊次座標由矩陣運算:
m ' * [x, y, z, 1]T = [2nx/(right-left) + (right+left)z/(right - left), 2ny/(top - bottom)+(top+bottom)z/(top-bottom), -az-b, -z]T
能夠獲得矩陣m'爲:
那麼如今m'就是咱們最終直接變換到ndc座標系下的變換矩陣。
這裏能夠看到這個變換矩陣徹底由視錐體的六個參數構成。咱們在轉換到ndc的過程當中,對於x和y首先中間轉換到投影平面座標,因爲投影后的z丟失,因此使用一個跟原來1/z成線性關係的表達式對應到Z(ndc)獲得a跟b的值,而後由投影屏幕座標與ndc的一個對應關係獲得最終變換到ndc座標系下X(ndc),Y(ndc)與視座標系中x,y的對應關係,最終獲得終極的透視矩陣。
而一般的類庫api都會提供設置視錐體的方法,好比gl-matrix:
fovy對應的角度稱爲俯仰角:
根據關係三角函數能夠算出top;
aspect爲寬高比:width/height用來根據top計算出left和right;
5、屏幕座標變換
與以前的步驟不一樣,視口變換不是由矩陣變換產生的。在這裏咱們使用webgl的viewport函數。變換函數爲:
function fromSreenToNdc(x, y, container) { return { x: x / container.offsetWidth * 2 - 1, y: -y / container.offsetHeight * 2 + 1, z: 1 }; } function fromNdcToScreen(x, y, container) { return { x: (x + 1) / 2 * container.offsetWidth, y: (1 - y) / 2 * container.offsetHeight }; }
這裏能夠看到aspect最好設置爲canvas.offsetWidth/canvas.offsetHeight,經過前面的圖能夠知道投影面多是矩形面,而ndc是正方形,因此投影過程當中會產生變形,而ndc到屏幕座標中也是會產生變形,也就是咱們讓投影面到ndc有變形,而後讓ndc到屏幕在變形回去,這樣就能保證最終顯示在屏幕上的3d物體保持原來比例。
參考資料:
[
](https://learnopengl-cn.github.io/01%20Getting%20started/07%20Transformations/)