目錄html
經過以前的教程,對WebGL中可編程渲染管線的流程有了必定的認識。可是隻有前面的知識還不足以繪製真正的三維場景,能夠發現以前咱們繪製的點、三角形的座標都是[-1,1]之間,Z值的座標都是採用的默認0值,而通常的三維場景都是很複雜的三維座標。爲了在二維視圖中繪製複雜的三維場景,須要進行相應的的圖形變換;這一篇教程,就是詳細講解WebGL的圖形變換的過程,這個過程一樣也適合OpenGL/OpenGL ES,甚至其餘3D圖形接口。編程
能夠用照相機拍攝照片來模擬這個圖形變換的過程,若是要對某個物體拍攝照片,大體過程以下:函數
而在WebGL/OpenGL中,具體的圖形變換流程以下所示[3]:
其中模型變換、視圖變換、投影變換是咱們本身在着色器裏定義和實現的,而視口變換通常是WebGL/OpenGL自動完成的。這就好像咱們拍照的時候,須要本身去調整位置,相機鏡頭焦距,而成像的過程就交給相機。因此模型變換、視圖變換、投影變換這三者特別重要,另外附一張WebGL/OpenGL矩陣變換的流程圖[4]:
學習
從上兩圖中能夠發現,場景中的物體老是從一個座標系空間轉換到另一個座標系空間。spa
在參考文獻[2]中描述的WebGL/OpenGL整個圖形變換過程的座標系和單位:
.net
其流程與前文論述的基本一致,能夠看到投影變換以後的過程不是那麼簡單,還須要將獲得的齊次裁剪座標作透視除法(除以w),作剪切和視口/深度範圍變換,光柵化等。設計
其中,用戶/着色器變換(也就是教程要具體詳述的模型變換、視圖變換和投影變換)包含座標系和單位以下所示:
3d
在一個三維軟件中瀏覽一個三維物體時候,老是會提供給用戶平移、縮放和旋轉的交互操做,而這正是模型變換的內容。在圖形學的範疇當中,平移變換、旋轉變換屬於剛體變換,縮放和旋轉屬於線性變換,剛體變換和線性變換又屬於仿射變換,而仿射變換也能夠當作投影變換的一種[5]。
code
也就是說這些圖形變換,本質上能夠當作是同一種變換;在數學上,可使用矩陣來描述這種變換。而且,爲了兼容各類變換的特殊性,會在3維的基礎上再加一維,使用4維的向量和矩陣。4維向量表述一個點(x,y,z,w)等價於三維向量(x/w,y/w,z/w),這就是前面提到的齊次座標。orm
具體來講,對於空間某個點v0(x0,y0,z0,1),通過空間圖像變換後獲得新的點v1(x1,y1,z1,1),那麼存在這樣一個4行4列的矩陣M:
\[ M= \left[ \begin{matrix} a & b & c & d \\ e & f & g & h\\ i & j & k & l\\ m & n & o & p\\ \end{matrix} \right] \]
模型變換包括平移變換、縮放變換和旋轉變換。從內容上來說,這幾種變換正好應對的三維交互操做的平移、變換和縮放。經過鼠標操做調整模型變換矩陣就能夠實現一種簡單三維交互操做。
對於一個點(x,y,z,1),平移以後,獲得的點就是(x+Tx,y+Ty,z+Tz,1),其中Tx、Ty、Tz分別表示點在X軸、Y軸、Z軸方向上移動的距離。那麼將其代入方程組式(2)的兩邊,有:
\[\begin{cases} a*x +b*y +c*z + d =x+Tx\\ e*x +f*y +g*z +h =y+Ty\\ i*x +j*y +k*z + l =z+Tz\\ m*x +n*y +o*z + p =1 \end{cases} \]
對於一個點(x,y,z,1),以原點爲中心縮放,在X方向縮放Sx倍,在Y方向縮放Sy倍,在Z方向縮放Sz倍,那麼新的座標值爲(x*Sx,y*Sy,z*Sz,1)。將其代入方程組式(2)的兩邊,有:
\[\begin{cases} a*x +b*y +c*z + d =x*Sx\\ e*x +f*y +g*z +h =y*Sy\\ i*x +j*y +k*z + l =z*Sz\\ m*x +n*y +o*z + p =1 \end{cases} \]
旋轉變換就稍微複雜一點,對旋轉變換而言,必須知道旋轉軸、旋轉方向和旋轉角度。能夠繞X軸,Y軸和Z軸旋轉,因此通常都會有三個旋轉矩陣。以繞Z軸旋轉爲例,在Z軸正半軸沿着Z軸負方向進行觀察,若是看到的物體是逆時針旋轉的,那麼就是正旋轉,旋轉方向就是正的,旋轉值就是正數;反之若是旋轉值爲負數,說明旋轉方向就是負的,沿着順時針旋轉。用更加通用的說法來講,正旋轉就是右手法則旋轉:右手握拳,大拇指伸直並使其指向旋轉軸的正方向,那麼右手其他幾個手指就指明瞭旋轉的方向。
對於一個點p(x,y,z,1),繞Z軸旋轉,由於旋轉後的Z值不變,因此能夠忽略Z值的變換,只考慮XY空間的變化。此時設r爲原點到點p的距離,α是X軸旋轉到該點的角度。如圖所示:
那麼p點的座標表示爲式(3):
\[\begin{cases} x=r*cosα\\ y=r*sinα\\ \end{cases} \tag{3} \]
使用矩陣來描述圖形變換的好處之一就是可以將以上全部的變換組合起來,例如以下式(6):
\[ v1=S*(R*(T*v0)) \tag{6} \]
表達的圖形變換是對於點v0,首先通過平移變換,再通過旋轉變換,最後再進行縮放,獲得新的點v1。
根據矩陣乘法的結合律,式(6)能夠寫成:
\[ v1=(S*R*T)*v0 \]
那麼模型矩陣M就能夠表示爲:
\[ M=S*R*T \]
注意上述模型矩陣的SRT順序並非固定的,須要根據實際的狀況採起合適的矩陣,不然會達不到想要的效果。一個重要的原則就是記住縮放變換老是基於原點的,旋轉變換老是基於旋轉軸的,在進行縮放變換和旋轉變換以前每每須要先平移變換至原點位置(不是絕對)。
視圖變換其實就是模型變換的逆變換。試想一下,拿一個物體給相機拍攝,其實也就是拿相機去拍攝一個物體,視圖變換和模型變換的結果並無顯著的區別,有些狀況下二者甚至能夠合併成一個模型-視圖變換(model-view transform)。二者之因此須要分開進行徹底是由實際的交互操做決定的:旋轉、縮放到合適的位置實際上是很難設置的,不少交互操做須要在視空間/攝像機空間中設置才比較合適,這個時候就須要視圖變換了。
視圖變換其實就是構建一個視空間/攝像機空間,須要三個條件量:
經過上述三個條件量,就能夠構建一個視圖矩陣。這個矩陣通常能夠經過圖形矩陣庫的LookAt()函數進行設置,例如在WebGL的cuon-matrix.js中,其設置函數爲:
由前文得知,視圖變換構建了一個視空間/攝像機空間座標系,爲了對應於世界座標系的XYZ,能夠將其命名爲UVN座標系,它由以前提到的三個條件量構建而成:
如圖所示[7]:
因爲視圖變換是模型變換的逆變換,以上視圖變換的效果,等價於進行一個旋轉變換,再進行一個平移變換。故有視圖矩陣V:
\[ V=M^{-1}=(TR)^{-1}=R^{-1}T^{-1} \]
根據以前平移矩陣的定義,那麼有:
\[ T^{-1}= \left[ \begin{matrix} 1 & 0 & 0 & -Tx \\ 0 & 1 & 0 & -Ty\\ 0 & 0 & 1 & -Tz\\ 0 & 0 & 0 & 1\\ \end{matrix} \right] \]
這裏的(Tx,Ty,Tz)就是視點eye(eyeX, eyeY, eyeZ)。通過平移變換以後,相機的原點就和世界原點重合,剩下的操做就是經過旋轉矩陣R,將世界座標系XYZ的點轉換到成UVN座標系上的點。令:
\[ X=(1,0,0),Y=(0,1,0),Z=(0,0,1)\\ U=(Ux,Uy,Uz),V=(Vx,Vy,Vz),N=(Nx,Ny,Nz) \]
則有:
\[ \left[ \begin{matrix} U & V & N \\ \end{matrix} \right] = \left[ \begin{matrix} X & Y & Z \\ \end{matrix} \right] * R = \left[ \begin{matrix} X & Y & Z \\ \end{matrix} \right] * \left[ \begin{matrix} Ux & Vx & Nx \\ Uy & Vy & Ny \\ Uz & Vz & Nz \\ \end{matrix} \right] \]
又由旋轉矩陣R爲正交矩陣,因此有:
\[ R^{-1} = \left[ \begin{matrix} Ux & Uy & Uz \\ Vx & Vy & Vz \\ Nx & Ny & Nz \\ \end{matrix} \right] \]
最後便可得視圖矩陣:
\[ V=R^{-1} T^{-1}= \left[ \begin{matrix} Ux & Uy & Uz & 0 \\ Vx & Vy & Vz & 0 \\ Nx & Ny & Nz & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] * \left[ \begin{matrix} 1 & 0 & 0 & -Tx \\ 0 & 1 & 0 & -Ty\\ 0 & 0 & 1 & -Tz\\ 0 & 0 & 0 & 1\\ \end{matrix} \right] = \left[ \begin{matrix} Ux & Uy & Uz & -U·T \\ Vx & Vy & Vz & -V·T \\ Nx & Ny & Nz & -N·T \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]
投影變換定義的是一個可視空間,決定了哪些物體顯示,哪些物體不顯示,以及物體如何顯示。經常使用的可視空間有兩種:
投影投影模擬的就是人眼成像或者攝像機成像的過程,試想一下,攝像機拍攝的老是取景器方位內的物體,而且呈現近大遠小的效果。在WebGL/OpenGL中,透視投影就決定了一個視點、視線、近裁剪面、遠裁剪面組成的四棱椎可視空間。如圖所示:
在實際使用中,圖形矩陣庫(我這裏用的WebGL的cuon-matrix.js)通常都會提供相似setPerspective()的函數,具體定義以下:
如圖所示,已知視空間座標系XYZ,座標系原點(視點)爲O,視椎體近截面與視點距離爲n,遠平面與視點的距離爲f。已知視椎體空間中有一點爲P(x0,y0,z0),那麼要求的就是射線OP與近截面的投影點P1(x1,y1,z1)。如圖所示:
近截面與平面XOY平行,那麼z1 = -near,那麼問題能夠簡化爲:已知空間上點P的座標,存在點P與座標O連線上一點P1,P1的Z值已知,求P1座標。如圖所示:
顯然這是一個三角形類似的問題,P1點在視空間座標系的XY座標爲:
\[ \begin{cases} x1'=-n/z0*x0\\ y1'=-n/z0*y0\\ \end{cases} \]
根據前文論述,投影變換獲得的4維度齊次座標(x1,y1,z1,w1),會除以w1使得x1和y1的值歸一化到-1到1之間。那麼可設l和r分別爲近截面左、右邊框的x座標,那麼就是l映射到-1,r映射到1。這是一個線性變換問題:存在兩組點(l,-1)(r,1)知足方程y=kx+b。
\[ \begin{cases} kl+b=-1\\ kr+b=1\\ \end{cases} \]
解方程組:
\[ \begin{cases} k=\frac{2}{r-l}\\ b=-\frac{r+l}{r-l}\\ \end{cases} \]
那麼P1歸一化後的x座標xn爲:
\[ xn=\frac{2}{r-l}*x1'-\frac{r+l}{r-l}=-\frac{1}{z0}*(\frac{2n}{r-l}*x0+\frac{r+l}{r-l}*z0) \]
同理可得,P1歸一化以後y 座標yn爲:
\[ yn=-\frac{1}{z0}*(\frac{2n}{t-b}*y0+\frac{t+b}{t-b}*z0) \]
能夠發現,歸一化的座標xn、yn都存在一個乘數因子(-1/z0),那麼能夠令投影變換後的w1=-z0,這樣就能夠知足歸一化以後的wn=1,而且知足上面xn、yn的表達式。即有裁剪座標系的點P1(x1,y1,z1,w1):
\[ \begin{cases} x1= \frac{2n}{r-l}*x0+\frac{r+l}{r-l}*z0 \\ y1= \frac{2n}{t-b}*y0+\frac{t+b}{t-b}*z0 \\ w1= -z0 \\ \end{cases} \]
代入到式(2)中,得:
\[ \left[ \begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ I & J & K & L \\ 0 & 0 & -1 & 0 \\ \end{matrix} \right] * \left[ \begin{matrix} x0 \\ y0 \\ z0 \\ 1 \\ \end{matrix} \right] = \left[ \begin{matrix} x1 \\ y1 \\ z1 \\ w1 \\ \end{matrix} \right] \]
繼續求上式的投影矩陣的第三行。投影轉換後獲得的z1是一個深度值,它是一個與x0,y0無關的值,因此I=0,J=0。而且在歸一化以後,z1會成爲一個-1到1之間的值:當z0=-n時(近截面),z1=-1;當 z0=-f時(遠截面),z1=1。代入上式,有:
\[ \begin{cases} (K*(-n)+L)/n=-1 \\ (K*(-f)+L)/f=1 \\ \end{cases} \]
獲得:
\[ \begin{cases} K=(f+n)/(n-f) \\ L=2fn/(n-f) \\ \end{cases} \]
綜合,可得透視投影矩陣P:
\[ P= \left[ \begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & \frac{f+n}{n-f} & \frac{2fn}{n-f} \\ 0 & 0 & -1 & 0 \\ \end{matrix} \right] \]
注意,經過相似setPerspective()的函數定義的矩陣是對稱的視錐體,視點在近截面的投影點爲近截面的中心,於是有:
\[ \begin{cases} r=-l \\ t=-b \\ t-b=height \\ width= height*aspect \\ tan(\frac{fovy}{2})=\frac{height/2}{n} \end{cases} \]
代入透視投影矩陣P,獲得對稱透視投影矩陣P:
\[ P= \left[ \begin{matrix} \frac{1}{aspect*tan(\frac{fovy}{2})} & 0 & 0 & 0 \\ 0 & \frac{1}{tan(\frac{fovy}{2})} & 0 & 0 \\ 0 & 0 & \frac{f+n}{n-f} & \frac{2fn}{n-f} \\ 0 & 0 & -1 & 0 \\ \end{matrix} \right] \]
正射投影一個很常見的應用就是地圖。不管是紙質地圖仍是谷歌地圖,甚至於室內設計的戶型圖、工程設計的工程圖,無一例外所有都是正射投影。正射投影可以很方便的比較場景中物體的大小,而且每一個地方的所表明的大小都是同樣的(分辨率一致)。固然,在這種投影下是沒有深度感的,就像你在衛星地圖上是看不出一座山有多高的。
正射投影一樣也是近裁剪面和遠裁剪面組成的可視空間,只不過這個可視空間是個長方體,如圖所示:
一樣的,可使用相似setOrtho()函數來設置正射投影:
在正射投影的盒狀可視空間中,XYZ三個方向上都是等比例的。設盒狀可視空間中某一物體點P(x0,y0,z0),那麼P點在近截面的投影點爲P1(x0,y0,z0’),僅僅只是Z值不一樣。
同透視變換的推導同樣,將P1的X、Y座標(x0,y0)映射到-1到1的範圍(xn,yn)。即有兩組點(l,-1)和(r,1)知足式子(線性關係y=kx+b):
\[ Xn=Kx*x0+Bx \]
有兩組點(b,−1)和(t,1)知足式子(線性關係y=kx+b):
\[ Yn=Ky*y0+By \]
分別代入解方程組,可得:
\[ \begin{cases} xn=2/(r-l)*x0-(r+l)/(r-l) \\ yn=2/(t-b)*y0-(t+b)/(t-b) \\ \end{cases} \]
一樣的,在Z方向上,將z0映射成-1到1直接的值:當點在近截面時,映射成-1;當點在遠截面時,映射成1。故也有兩組點(-n,-1)和(-f,1)知足線性關係y=kx+b,同理可求得:
\[ zn=(-2)/(f-n)*z0-(f+n)/(f-n) \]
對於正射變換而言,w變量是沒必要要的,可直接令w=1。那麼裁剪座標P1(x1,y1,z1,w1)就是通過透視除法的標準化設備座標(xn,yn,zn,1)。故有:
\[ \begin{cases} x1=2/(r-l)*x0-(r+l)/(r-l) \\ y1=2/(t-b)*y0-(t+b)/(t-b) \\ z1=(-2)/(f-n)*z0-(f+n)/(f-n) \\ w1=1 \\ \end{cases} \]
代入到式(2)的兩邊,可得正射投影矩陣:
\[ O = \left[ \begin{matrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & -\frac{2}{f-n} & -\frac{f+n}{f-n} \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]
綜上所述,模型矩陣M,視圖矩陣V,投影矩陣P,同時做用於物體的頂點,使得最終的物體能後被看見或者進行UI操做。根據以前教程內容,逐頂點的操做能夠將其放入到頂點着色器。通常而言,先進行模型變換,再進行視圖變換,最後進行投影變換:
\[ v1=P*V*M*v0 \]
根據矩陣乘法的結合律:
\[ v1=(P*V*M)*v0 \]
這個P*V*M矩陣合併獲得的模型視圖投影矩陣(model view projection matrix),簡稱爲MVP矩陣。在實際使用過程當中,只須要將這個MVP矩陣傳入到頂點着色器,就能根據設置的矩陣獲得想要的渲染效果:
gl_Position = u_MvpMatrix * a_Position;
這一篇教程是純理論知識,相對來講不太容易理解。若是是初次接觸,至少應該先作大體的瞭解,後續會大量用到這裏的知識。
[1]《WebGL編程指南》
[2]《OpenGL編程指南》第八版
[3] OpenGL學習腳印: 投影矩陣和視口變換矩陣(math-projection and viewport matrix)
[4] OpenGL矩陣變換的數學推導
[5] 基本圖像變換:線性變換,仿射變換,投影變換
[6] 旋轉變換(一)旋轉矩陣
[7] 視圖矩陣的推導