本節內容來自於小冊 WebGL 入門與實踐。javascript
上一節咱們講了 WebGL 座標系的分類以及它們之間的轉換方式,本節開始詳細介紹座標系基本變換的算法實現,圖形學中實現變換的主要數學工具是矩陣
,因此在講解座標系變換以前,咱們先溫習一下矩陣。前端
舒適提示:java
在學習矩陣變換時,必定要搞清楚如下三點:算法
行向量
仍是列向量
。
行向量
,按照數學領域
中矩陣相乘的規則,向量要放在左側
相乘。列向量
,向量要放在右側
相乘。行主序
仍是列主序
。
第一行
第一列
在多個矩陣變換時,不一樣的相乘順序會致使不一樣的結果,因此咱們要保證矩陣相乘的順序是咱們指望的。假設有三個變換矩陣:旋轉矩陣 R,平移矩陣 T,縮放矩陣 S,以及頂點向量 P,那麼 P 變換到 P1 的順序通常是這樣的:canvas
即先縮放
,再旋轉
,最後平移
。數組
3D 學習過程當中的一大難點就是矩陣變換,咱們常常看到矩陣左乘一個列向量就可以實現平移、旋轉、縮放等效果。框架
那麼,矩陣背後的神祕力量是什麼呢?函數
其實矩陣並不神祕,只是矩陣能夠對一些數字按照矩陣的規則執行一系列運算操做,簡化了咱們使用+
、 -
、 *
、 /
進行變換運算的步驟而已。工具
一個矩陣能夠理解爲一種變換,多個矩陣相乘表明多個變換。學習
常見的矩陣變換有以下幾種:
可是在座標系轉換中,最常應用的是前三種。咱們看一下如何用矩陣表示這些變換。在講解矩陣變換以前,咱們先從頭捋一下點
和向量
的表示。
前面章節咱們講了齊次座標
,它用來區分點
和向量
,齊次座標使用 N+1
維向量表示 N 維空間,第 N+1 維數字若是是 0 的話,則表明 N 維空間中的向量
,以下用 4 維向量表示 3 維空間中的一個向量:
第 N+1
維數字若是是非0數字
的話,則表明 N 維空間下的點
:
使用 N+1 維數字表示 N 維空間中的點或向量的方式就是齊次座標。
齊次座標除了可以區分點
和向量
,還有兩大用處:
模擬透視效果咱們上一節已經介紹了,在裁剪座標系中,w 值越大,通過透視除法後的座標越小,因而也就有了近大遠小的投影效果。
前面章節已經講過,n 階矩陣只能和 n 維列向量相乘,獲得一個新的 n 維列向量。
乘得的結果只能表示縮放和旋轉變換,沒有辦法表示平移變換,由於平移是在原向量的基礎上加上一個常量位移,屬於加法操做,可是 n 階矩陣和 n 維列向量相乘的話,每一步都是相乘操做,沒有加法運算,因此沒法用 n 階矩陣和 n 維列向量表示 n 維列向量的平移。
要注意:上面所說的列向量指的是座標,不是數學意義上的向量。
能夠看到 n 維矩陣和 n 維向量相乘,不能實現 n 維向量和一個常量進行加減的操做。
咱們期待的是獲得這樣一個向量:
其中 p 是常數,表明平移的大小。
咱們看一下齊次座標是如何幫助咱們解決這個問題的。
頂點 P 用齊次座標表示以下:
由於 3 維座標用齊次座標的話須要增長到 4 維,因此表示平移變換的矩陣也要相應地變成 4 階矩陣,咱們看下這個 4 階矩陣如何構成:
在原來基礎上增長一行和一列,其中第四行前三個份量爲 0,第四個份量爲 1,這樣矩陣和向量的乘積獲得的新的向量的第四個份量也是 1,因此也是表明點。
第四列t一、t二、t3分別表明沿 x 軸、y 軸、 z 軸方向上的平移量。
咱們推算驗證一下:
轉換後的向量的每個份量都實現了ax + by + cz + 常數
的格式,也就是說,向量能夠經過乘以一個矩陣實現平移操做。
首先咱們要知道,對物體(頂點)作平移、旋轉、縮放的變換操做至關於對原來的座標系作平移、旋轉、縮放變換,獲得一個新座標系。瞭解了這一點,咱們就能夠學習一種求解變換矩陣的簡單方式:
基向量是指座標系中各個座標軸正方向的單位向量,假設 Ux 表明 X 軸的單位向量,那麼 Ux = (1, 0, 0),同理, Uy = (0, 1, 0),Uz = (0, 0, 1)。
基向量是座標系變換的基礎,咱們求解座標變換矩陣關鍵就是要找到原座標系的基向量在新座標系中的表示。
這是一個簡單易於理解的求解思路,掌握了這個思路,無論進行什麼樣的變換,咱們都能很快地求出來變換矩陣,只須要找到這些值,而後將其代入矩陣框架
就行啦。
下面是一個座標系變換的例子,座標系 oxyz 繞 Z 軸旋轉 β 角度後造成了新座標系 ox'y'z':
你們必定要分清,新座標系是 ox'y'z'
,原座標系是 oxyz
,新座標系的基向量
在原座標系下的表示咱們利用三角函數運算便可求出,如上圖所示,因此按照求解思路的第一步,新座標系的基向量在原座標系表示爲:
原座標系的座標原點和新座標系重合,因此新座標系原點在原座標系下的表示:
代入通用矩陣框架後得出變換矩陣爲:
按照上面的矩陣變換求解思路來尋找平移矩陣:
一、求出原座標系的基向量在新座標系的表示。
因爲沒有進行旋轉和縮放操做,因此新座標系的基向量和原座標系同樣:
二、新座標系座標原點的座標:
將這些值代入變換矩陣框架
咱們用 JavaScript 實現上述平移矩陣。
還記得嗎?WebGL 矩陣是列主序的,每隔 4 個數表明一列。
function translation(tx, ty, tz, target){
target = target || new Float32Array(16);
// 第一列
target[0] = 1;
target[1] = 0;
target[2] = 0;
target[3] = 0;
// 第二列
target[4] = 0;
target[5] = 1;
target[6] = 0;
target[7] = 0;
// 第三列
target[8] = 0;
target[9] = 0;
target[10] = 1;
target[11] = 0;
// 第四列
target[12] = tx;
target[13] = ty;
target[14] = tz;
target[15] = 0;
return target;
}
複製代碼
平移矩陣的生成算法很簡單,按照數學關於矩陣的定義,在指定位置設置正確的值便可。
以後咱們就能夠用該算法生成一個平移矩陣實現頂點的平移變換了。
平移矩陣的演示
咱們繪製兩個半徑爲 5 的球體,第一個球體在世界座標系中心,第二個球體沿着 X 軸偏移 10 個單位,爲了演示方便,咱們先設置一個正射投影矩陣,左平面位於 -15 處,右平面位於 15 處,上平面位於 15 處,下平面位於 -15 處,遠平面位於 1000,近平面位於 -1000。
//獲取視口寬高比
var aspect = canvas.width / canvas.height;
//獲取正射投影觀察箱
var perMatrix = matrix.ortho(-aspect * 15, aspect * 15, -15, 15, 1000, -1000);
// 獲取平移矩陣
var translationMatrix = matrix.translation(10, 0, 0);
// 將矩陣傳往 GPU。
gl.uniformMatrix4fv(u_Matrix, translationMatrix);
複製代碼
一、gl.uniformMatrix4fv該方法的做用是 JavaScript 向着色器程序中的`u_Matrix`屬性傳遞一個 4 階`列主序`矩陣。
二、ortho 方法是生成正射投影矩陣的方法,講到投影變換時咱們再對它的實現作講解。
複製代碼
右側球體是平移後的效果:
能夠看出,平移矩陣可以正常工做。
縮放是將組成物體的各個頂點沿着對應座標軸縮小或者放大,一種方法是:使用頂點向量乘以縮放向量便可實現。請注意數學領域向量和向量只有點乘和叉乘,並無一種運算能夠實現向量與向量各個份量相乘獲得一個新的向量。 不過在上一節咱們使用 JavaScript 實現了這樣一個算法,在這裏就能夠用到了:
Vector3 vec = new Vector3(3, 2, 1);
Vector3 scale = new Vector3(2, 2, 1);
vec = vec.multiply(scale);
複製代碼
可是這裏咱們要實現的是經過向量和矩陣相乘的方式來實現。
咱們要構建一個縮放矩陣,縮放矩陣也比較簡單,按照上面的求解思路:
一、新座標系基向量在原座標系下的表示:
沿着 X 軸縮放 sx 倍,至關於將原來的基向量放大了 sx 倍,因此新座標系下一個單位的長度至關於原來座標系下的 sx 個長度,以此類推,咱們很容易地推導出 Y 軸和 Z 軸的基向量
二、原座標系原點在新座標系下的座標:
因爲縮放操做沒有改變原點位置,因此,原點座標在新座標系下仍然是(0,0,0)。
將這些值代入變換矩陣框架,能夠得出:
上面這個圖就是一個典型的縮放矩陣:
function scale(sx, sy, sz, target){
target = target || new Float32Array(16);
// 第一列
target[0] = sx;
target[1] = 0;
target[2] = 0;
target[3] = 0;
// 第二列
target[4] = 0;
target[5] = sy;
target[6] = 0;
target[7] = 0;
// 第三列
target[8] = 0;
target[9] = 0;
target[10] = sz;
target[11] = 0;
// 第四列
target[12] = 0;
target[13] = 0;
target[14] = 0;
target[15] = 1;
return target;
}
複製代碼
調用該方法須要指定三個方向的縮放比例,可是有時咱們可能只縮放某個方向,因此須要再衍生三個縮放函數
function scaleX(sx){
return scale(sx, 1, 1);
}
function scaleY(sy){
return scale(1, sy, 1);
}
function scaleZ(sz){
return scale(1, 1, sz);
}
複製代碼
相比平移和縮放,旋轉矩陣相對複雜一些,咱們從 2D 平面上一個頂點的旋轉提及。
點 P(x, y) 旋轉 β 角度後,獲得一個新的頂點 P1(x1, y1) , P1 和 P 之間的座標關係:
P 點座標:
旋轉後的 P1 點座標:
將 P 點座標帶入 P1點能夠獲得:
咱們使用齊次座標和矩陣表示:
擴展到 3D 空間,咱們一樣能推導出下面三種旋轉矩陣。
JavaScript 的實現,相信你們已經熟記於心了,咱們只須要在矩陣的各個位置指定對應數字便可。
function rotationX(angle, target){
target = target || new Float32Array(16);
let sin = Math.sin(angle);
let cos = Math.cos(angle);
target[0] = 1;
target[1] = 0;
target[2] = 0;
target[3] = 0;
target[4] = 0;
target[5] = cos;
target[6] = sin;
target[7] = 0;
target[8] = 0;
target[9] = -sin;
target[10] = cos;
target[11] = 0;
target[12] = 0;
target[13] = 0;
target[14] = 0;
target[15] = 1;
return target;
}
複製代碼
只要你理解了矩陣的運算規則,並推導出變換矩陣,以後只需將各個位置的元素賦值到一個類型化數組中便可。
算法和 X 軸旋轉極其類似,就不在這裏寫了,具體實現請看這裏。
具體實現請看這裏。
請注意:以上每一種旋轉都是單一旋轉,但每每咱們須要既沿 X 軸旋轉,又要沿 Y 軸旋轉,這種狀況,咱們只須要將旋轉矩陣相乘,獲得的新的矩陣就是包含了這兩種旋轉的變換矩陣。
上面三種是繞座標軸進行旋轉,但實際上咱們每每須要繞空間中某一根軸旋轉,繞任意軸旋轉的矩陣求解比較複雜。
這裏咱們採用過原點的任意軸旋轉,不考慮平移狀況,若是是繞一個不過原點的任意軸旋轉的話,咱們能夠利用一個旋轉矩陣和一個平移矩陣來完成。
咱們看下如何推導,以下圖所示:
點 C 旋轉 β 角以後,來到 C' 點。
假設這個變換矩陣爲 M,那麼 M 和 角度 β以及旋轉軸 有關,
以下圖所示:
咱們如今須要求得如何用點C、 旋轉角β以及旋轉軸 表示 C'。
用數學公式表示:
一、向量點乘
,求出向量
。
二、求出
三、經過向量叉乘
求得
四、利用三角函數求出
五、利用向量加法求出
六、將1-4步代入第5步,得出:
假設旋轉軸向量表示爲:
新座標系基向量U(Ux,Uy,Uz)在原座標系中的座標位置求解以下:
利用向量點乘、叉乘規則最終推導出:
同理,將Y 軸基向量 (0, 1, 0) 代入上面公式,推導可得:
這樣咱們就求出了新座標系的基向量在原座標系的表示。
接下來,咱們找出新座標系的原點在原座標系下的座標,由於是繞原點旋轉,因此座標不變,仍然是(0,0,0)。將這些值代入矩陣框架,得出繞任意旋轉軸的變換矩陣:
有了變換矩陣,那麼咱們就能夠實現JavaScript的任意軸旋轉矩陣了:
function axisRotation(axis, angle, target){
var x = axis.x;
var y = axis.y;
var z = axis.z;
var l = Math.sqrt(x * x + y * y + z * z);
x = x / l;
y = y/ l;
z = z /l;
var xx = x * x;
var yy = y * y;
var zz = z * z;
var cos = Math.cos(angle);
var sin = Math.sin(angle);
var oneMCos = 1 - cos;
target = target || new Float32Array(16);
target[0] = xx + (1 - xx) * cos;
target[1] = x * y * oneMCos + z * sin;
target[2] = x * z * oneMcos - y * sin;
target[3] = 0;
target[4] = x * y * oneMCos - z * sin;
target[5] = yy + (1 - yy) * cos;
target[6] = y * z * oneMCos + x * sin;
target[7] = 0;
target[8] = x * z * oneMCos + y * sin;
target[9] = y * z * oneMCos - x * sin;
target[10] = zz + (1 - zz) * cos;
target[11] = 0;
target[12] = 0;
target[13] = 0;
target[14] = 0;
target[15] = 1;
return target;
}
複製代碼
以上就是繞任意軸進行旋轉的矩陣。
咱們利用上面的算法演示一下,使用四根旋轉軸:
// 中間立方體繞 X 軸旋轉。
var axisX = {x: 1, y: 0, z: 0}
//右邊立方體繞 Y 軸旋轉
var axisY = {x: 0, y: 1, z: 0}
// 左邊立方體繞 Z 軸旋轉
var axisZ = {x: 0, y: 0, z: 1}
// 上邊立方體繞對角線軸旋轉。
var axisXYZ = {x: 1, y: 1, z: 1}
複製代碼
效果以下:
繞任意軸旋轉的推導比較複雜,涉及到立體幾何以向量點乘叉乘等運算,不過它的使用方法仍是很簡單的。
本節主要講解座標系變換過程當中涉及到的基本變換的原理與實現,涉及幾何和三角函數的運算比較多,你們看一遍可能不能明白,不妨多看幾遍,拿紙筆寫寫畫畫,很快就會豁然開朗。
雖說這些 API 只要能看懂、會用就足夠了,沒有必要去掌握推導過程,但我仍然建議你們嘗試推導一遍,掌握推導過程對鞏固學過的數學知識頗有幫助,也能夠培養本身利用數學知識解決疑難問題的能力。
下一節咱們學習如何利用這些基本變換實現各個座標系之間的變換。
這一系列的內容來自於小冊 WebGL 3D 入門與實踐,若是你們對進階知識感興趣,能夠到小冊中去學習:小冊:WebGL 3D 入門與實踐。
小冊內容除了包含 WebGL 相關的基礎練習,還包括 3D 圖形概念與相關數學的原理與推導,旨在幫助你們創建圖形學的技術輪廓。這部分圖形學知識獨立於 WebGL,除了能夠適用於 WebGL,還適用於 OpenGL 等。
固然,本小冊主要目的仍是幫助 Web 前端同窗學習 3D 技術,除了介紹適用 WebGL 實現 3D 效果之外,還對 CSS3 中的 3D 技術相關屬性進行了深刻剖析,並演示了與數學庫的結合使用,但願可以讓前端同窗不只僅侷限於通常的二維平面開發,也可以在 3D 開發上更近一步~