實驗平臺:win7,VS2010html
先上結果截圖(文章最後下載程序,解壓後直接運行BIN文件夾下的EXE程序):編程
a.鼠標拖拽旋轉物體,相似於OGRE中的「OgreBites::CameraStyle::CS_ORBIT」。數組
b.鍵盤WSAD鍵移動鏡頭,鼠標拖拽改變鏡頭方向,相似於OGRE中的「OgreBites::CameraStyle::CS_FREELOOK」。框架
1.座標變換的一個例子,兩種思路理解多個變換的疊加ide
如今考慮Scale(1,2,1); Transtale(2,1,0); Rotate(pi/4,(0,0,1)); 這3個變換(下文用S, T, R簡寫),做用到原先中心位於原點邊長爲2的立方體上的狀況。函數
座標系顯示說明及變換前的場景以下:動畫
以上變換用OpenGL(經典管線)和GLM實現代碼分別以下:網站
glMatrixMode(GL_MODELVIEW); glPushMatrix(); glScalef(1, 2, 1); glTranslatef(2, 1, 0); glRotatef(45, 0, 0, 1); glutSolidCube(2); draw_frame(1.5f); glPopMatrix();
glm::mat4 t = glm::scale( glm::vec3(1,2,1) ) * glm::translate( glm::vec3(2,1,0) ) * glm::rotate( 45.0f, glm::vec3(0,0,1) ); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glMultMatrixf(&t[0][0]); glutSolidCube(2); draw_frame(1.5f); glPopMatrix();
變換後的場景以下圖:spa
如今,能夠用兩種思路來理解S(1,2,1); T(2,1,0); R(pi/4,(0,0,1)); 這三個變換的疊加。.net
全局座標變換,全部的變換在一個全局的固定的座標系下進行,全部操做均以這個座標系爲參考,注意縮放相對於原點進行,這時的變換順序和代碼順序正好相反,爲 R(pi/4,(0,0,1)); T(2,1,0); S(1,2,1); ;
座標系變換,變換針對座標系框架進行,全部操做以當前座標系爲參考,全部變換施加後獲得一個新座標系,在這個座標系中繪製物體,這時的變換順序和代碼相同,爲 S(1,2,1); T(2,1,0); R(pi/4,(0,0,1)); ;
兩種的圖示以下:
這裏有幾點須要說明或者強調一下。思路1全局座標變換(左圖),最後一步S(1,2,1);相對於全局座標系的原點,而不是物體中心,因此物體的中心發生了變化。思路2物體座標系變換(右圖),第2步T(2,1,0);以物體座標系爲參考,由於物體座標系的Y軸在上次變換中被拉長了,因此Y軸的1長度也被拉長了,第3步R(pi/4,(0,0,1));也以物體座標系爲參考,由於物體座標系的Y軸被拉長了,頂點的旋轉軌跡在咱們看來是個橢圓,如圖中青色所示,一樣,所旋轉的45度在咱們看來也不是45度(物體座標系並不知道這一點,它只根據當前座標系進行變換,而且老是以爲本身的旋轉軌跡是圓,角度是45度)。
這兩種思路顯然不是巧合,它們的背後有深入的數學原理,請接着看下一節。
2.座標變換的數學原理,兩種思路背後的數學解釋
由於涉及好多數學公式,這裏採用一種新的撰文形式,即PPT加講解的形式,每頁PPT截圖後面有旁白解釋。若是嫌截圖不清楚,文章最後給出了PPT的下載連接。
數域通常取實數集或複數集。基是一組線性無關的向量組,而不必定是互相正交(垂直)的。座標用列向量表示,在OpenGL中點的座標也用列向量表示。
基的定義,空間中任一貫量均能用基線性表示,另外一組基中的向量也如此。T爲n介方陣。注意這裏T是乘在右邊。
Y的公式同理直接寫出來了。X, Y均爲列向量。注意這裏T乘在Y的左邊,由於Y是列向量嘛,對比前一頁PPT,基變換公式中T乘在左邊,這種差別是關鍵,且往下看。目前講到的基變換與座標變換均爲線性變換(符合f(ax+by)=af(x)+bf(y)的稱爲線性),線性變換將原點變換爲原點,而OpenGL中的變換能夠平移,下面講到,這是仿射變換。
R表示實數集合,|A|表示A的行列式(determinant,有時也表示爲det(A))。限制A的行列式不爲0是要求A非奇異(not singular, invertible,可逆),由於A不可逆時可能將直線映射爲一點,即將n維空間壓縮爲小於n維。另外A的行列式若是爲負則變換產生鏡像(如將右手系變換爲左手系)。以前用X,Y表示座標也即列向量,如今用粗體小寫字母x,y,b表示列向量。PPT中用到了分塊矩陣表示,注意A爲n×n,x,y,b爲n×1,粗體0是1×n個0。xT表矩陣轉置(transposition)。擴充第n+1個座標是爲了可以表示平移,也就是說n+1維空間的線性變換能夠表示n維空間的平移。第n+1個座標還能夠用來分辨n維空間中的點與向量(或者說是方向),即第n+1個座標不爲0時表示點,爲0時表示向量,不爲0且不爲1時要將全部座標都縮放一個倍數使之爲1。第n+1個座標爲0時能夠從兩個角度理解,一是理解成兩個點的差,點的第n+1個座標都是1,作差後第n+1個座標爲0,兩個點的差也就是向量,二是將其理解爲第n+1個座標w是從1逼近0,這時能夠表示無窮遠處的點,也就是一個方向。注意這裏的變換矩陣T有固定的形式,即最後一行爲n個0接1個1,仿射變換隻是n+1維空間的特殊線性變換(自由度小於(n+1)2小於等於n2+n)。若是T的最後一行的前n個元素不爲0,那麼變換可能將直線變爲曲線(請自行舉例),即變換後的座標是原座標的有理分式(這在OpenGL投影矩陣中被應用)。
原基爲向量,前n個元素是齊次座標系中的向量,這裏將線性變換(沒有平移)推廣到仿射變換,即加入原點,原點是新基中惟一的點(其餘爲向量)。
這裏將以前用的字母T改用A,如今的T表示齊次座標下的變換矩陣(見PPT第2頁)。注意b實際上是第一組基下的座標(和A同樣),這組座標和基相乘獲得它表示的向量。這裏再次注意T在基變換和座標變換公式中的位置。強調一下,T並非自由的n+1介方陣,它的最後一行固定爲n個0接1個1。第n+1個座標w爲0時表示的向量(認爲是兩個點的差),向量的仿射變換能夠當作是其兩個端點仿射變換後作差,這時T的平移部分將被抵消(請看下一節仿射變換的分解),也就是說w爲0的向量的仿射變換隻和T的旋轉和縮放部分有關(也就是自由向量的概念,向量只有方向和大小,沒有起點)。
這裏順便提一下,矩陣相乘的幾何意義就是變換的疊加,即線性映射的疊加。注意一個細節,這裏的每一個T既表示仿射變換自己,又表示仿射變換的變換矩陣,並無加以區分,這是合理的,由於仿射變換和仿射變換矩陣之間有一一對應的關係(全部仿射變換構成的空間和全部仿射變換矩陣構成的空間同構)。至此完全瞭解了兩種思路的數學原理。再次強調這裏的仿射變換T可能不必定是剛體變換,它有可能產生縮放、錯切變形。第1節的例子就不是剛體變換,以上的兩種思路和解釋是對仿射變換成立的,不限於剛體變換(旋轉和平移或其疊加)。
3.更深刻的數學,座標變換的分解(矩陣的分解)
接着用PPT的形式~
這裏都講的是三維空間。I表示單位矩陣(identity matrix,數學書中通常用E表示)。||v||表示範數,在向量空間中也就是向量的模長。注意到 (u·x)u=(uuT)x,u×x(叉乘)等於 u波浪線 矩陣乘 x,旋轉公式只要選定 u, u×x, x-(u·x)u 三個新基就很好看懂了(文獻[1]第11頁)。這裏既用字母T表示仿射變換矩陣,又用其表示平移矩陣,T的具體含義能夠根據上下文區分不會混淆,用C++術語來講,它們的參數列表不一樣。旋轉矩陣沿xyz軸的特殊形式請自行將v設爲特殊值進行推導。那如今的問題是,任意給一個仿射變換矩陣T(要符合最後一行是n個0接1個1),T可否分解爲T(x,y,z), S(x,y,z), R(a,(x,y,z))的組合(連乘積)呢?答案是確定的,請繼續往下看。
再次,既用T, R, S表示矩陣,又用其表示平移、旋轉、縮放函數,請很據上下文區分。行列式爲正的正交矩陣是一個旋轉矩陣,對稱矩陣是個縮放矩陣(縮放值可能有負值,這時產生鏡像,即手性變化),通過對角化後分解爲旋轉矩陣和沿xyz軸縮放的矩陣(即對角陣)。注意極式分解具備惟一性,對角化不具備惟一性,但不惟一性也僅限於調換對角陣的行或列(相應調換對角陣兩邊的旋轉矩陣)。能夠根據T, S, R(平移、縮放、旋轉)的逆來構造整個變換T的逆((AB)-1=B-1A-1,固然也能夠不分解直接求逆矩陣)。
4.圖形學中的變換模型以及OpenGL的實現
這裏講的變換模型是指一種「思惟模型」,也就是說用這個模型去思考能夠很方便對物體位置和定向進行操做,而具體的實現可以保證按照這個模型思考必定可以獲得正確答案,但這個實現可能根本就不是循序漸進的按照模型實現具體座標的計算,因此還要講OpenGL的實現。
圖形學中的變換模型通常涉及物體座標系(model space)、世界座標系(world space)、視覺座標系(eye space)、規範化設備座標系(normalized device space)、窗口像素座標系(window space),這些座標系中的座標相應叫作某某座標,如世界座標系中的座標叫作世界座標(world coordinates)。一個示意圖以下(用Blender軟件製做和渲染的):
如圖中所標註的,猴頭上面的座標框架表示物體座標系;那個最大的座標框架是世界座標系,水紅色的是地板;黑色的攝像機上的是視覺座標系,視覺座標系的定義是,鏡頭所指方向爲z負方向,攝像機正上爲y正方向,右手法則肯定x方向。
座標的變換以下(請見文獻[8]第66頁):
1.模型變換,視圖變換
如今舉例子說明物體座標到視覺座標的變換,場景是(請看上面猴頭那個圖),猴頭的中心位於物體座標系原點,猴頭中心位於世界座標系的(1,1,1)處,攝像機位於世界座標系的(0,1,5),攝像機的向上方向沿世界座標系y正方向,攝像機鏡頭對準世界座標系z負方向。對猴頭中心來講,它在物體座標系中座標(0,0,0),世界座標系中座標(1,1,1),視覺座標系中座標(1,0,-4)。模型變換矩陣爲T(1,1,1),視圖變換矩陣爲T(0,-1,-5),若是把模型和視圖矩陣合起來就是T(0,-1,-5)T(1,1,1)=T(1,0,-4)(還記得,T(x,y,z)表示平移)。GLM和OpenGL函數中的LookAt函數返回的變換矩陣是,將攝像機設置爲函數參數指定的狀況所須要的視圖矩陣。
2.投影變換
投影變換請見下圖(摘自文獻[4]):
投影變換後進入座標裁剪,即落在紅色方框外的部分將被裁剪掉。
3.透視除法
投影變換後齊次座標的第4個份量w可能不爲1,透視除法即將xyz份量都除以w,獲得規範化設備座標(特色是xyz份量範圍在-1到+1之間),對透視投影而言,這一步是非線性的(遠處物體被壓縮)。以下圖(摘自文獻[4]):
投影變換和透視除法合起來的效果是,將指定的視景體(也叫平截頭體,也就是那個裁剪框)變換爲邊平行於xyz軸且xyz範圍都是-1到+1中心位於原點的正方體。注意z座標的符號變化。以下圖(摘自文獻[7]):
4.視口變換
再通過視口變換,即調用OpenGL的glViewport函數,對應到窗口像素,注意,在OpenGL中,像素座標系的原點位於左下角,向右爲x軸正向上爲y軸正(而通常圖片像素都是以左上角爲原點)。具體來講,視口變換將規範化設備座標的位於[-1,1]之間的z座標對應到深度值,通常在[0,1](值越小離攝像機越近,z=-1對應d=0,z=+1對應d=1,d爲深度值),將(-1,-1,z)對應到屏幕(0,0,d)點,其中d爲深度值,將(1,1,z)對應到屏幕(w,h,d)點,其中w,h爲窗口的寬和高,其餘點按線性插值。以下圖(摘自文獻[4]):
5.OpenGL實現
具體到OpenGL的實現,OpenGL和數學中相同採用右手系,OpenGL把模型變換和視圖變換合二爲一,即模型視圖矩陣。OpenGL和GLM的變換矩陣都是按照列優先存儲在內存中,這和C++二維數組不一樣,其實,GLM中的4x4矩陣是由4個列向量組成的。按照上面的分析,當OpenGL的模型視圖矩陣和投影矩陣均爲單位陣時,這時攝像機位於世界座標系原點看向z負方向,向右方向沿x軸正方向,向上方向沿y正方向,因爲投影矩陣爲單位陣,這時爲正交投影(另外一種是透視投影),裁剪面爲xyz的±1,也就是說,對應到最後的顯示窗口,x方向向右,y方向向上,z方向垂直屏幕向外,窗口中心對應座標原點,窗口邊緣對應±1,而且z值小的片段遮擋z值大的片段(正好和離攝像機的遠近關係反了,這是由於沒有對z座標進行變號)。對了,OpenGL除了模型視圖矩陣和投影矩陣以外,還有文理座標變換矩陣和顏色變換矩陣。請見OpenGL官方手冊文獻[8]2.12和2.16。
5.兩種攝像機交互模型
如今用前面的知識實現兩種最多見的攝像機交互模型,先說下對上面說的變換模型的實現,程序有以下全局變量:
glm::mat4 transform_camera(1.0f); // 攝像機的位置和定向,即攝像機在世界座標系中位置 glm::mat4 transform_model(1.0f); // 模型變換矩陣,即物體座標到世界座標 glm::vec4 position_light0(0); // 光源位置,世界座標系中的座標 float speed_scale=0.1f; // 鼠標交互,移動速度縮放值
在繪製函數中,這些全局變量被應用以下(第一行之因此求逆,是由於model_view_matrix表示的是視覺座標到世界座標的變換矩陣,也就是攝像機在世界座標系中的位置,這裏須要的是將世界座標變換到視覺座標):
glm::mat4 model_view_matrix = glm::affineInverse(transform_camera); glMatrixMode(GL_MODELVIEW); glLoadMatrixf(&model_view_matrix[0][0]); glLightfv(GL_LIGHT0, GL_POSITION, &position_light0[0]); // 位置式光源 draw_world(10,3, true, true, true); // 繪製世界 model_view_matrix *= transform_model; glMatrixMode(GL_MODELVIEW); glLoadMatrixf(&model_view_matrix[0][0]); draw(); // 繪製物體
第一種拖拽球模型(姑且叫作拖拽球吧),它設想窗口中心有個虛擬的球,假設鼠標位於這個球面靠近咱們的半面,即z≥0的半球面(使用MeshLab軟件製做):
數學描述以下,圖中的 座標系是視覺座標系,對應到屏幕也就是向右爲x軸,向上爲y軸,垂直屏幕向外爲z軸:
推導旋轉矩陣以下:
這裏都假設 a, b 兩點在距離屏幕中心小於r的狀況,若是大於r(實際上是大於水紅色圓半徑)請見上圖的中的 p 點,用 p撇 代替 p 點,水紅色圓半徑小於r是但願鼠標在距離中心大於r的地方沿徑向移動物體也能產生旋轉。OpenGL官方Wiki上有個更好的解決方法,見文獻[9]。
第二種漫遊模型(姑且叫漫遊吧),要求按下鍵盤WSAD鍵攝像機前進、後退、左移、右移,鼠標左右移動時攝像機鏡頭左右掃動,鼠標上下移動時攝像機鏡頭作俯仰動,以下圖所示:
注意鼠標左右移動的旋轉軸要沿世界座標系的y軸旋轉,而不是攝像機本身的y軸,以防止視角傾斜,鼠標上下移動就沿攝像機本身的x軸旋轉就行。鍵盤WSAD鍵沿攝像機的z軸和x軸移動就行。
下面是程序實現,程序中的鍵盤和鼠標響應以下:
1.WS鍵,攝像機沿視覺座標系z軸移動,AD鍵,攝像機沿視覺座標系x軸移動;
transform_camera *= glm::translate( speed_scale*glm::vec3(dx,0,dz) );
2.上下鍵,攝像機沿世界座標系y軸移動,這裏v爲世界座標系的y軸單位向量(不是點,因此第四個份量爲0,這點很重要,若寫成vec4(0,1,0,1)將獲得錯誤結果)在視覺座標系中的座標;
glm::vec3 v = glm::vec3( glm::affineInverse(transform_camera)*glm::vec4(0,1,0,0) ); transform_camera *= glm::translate( speed_scale * dy * v );
3.左右鍵,攝像機沿視覺座標系z軸旋轉;
transform_camera *= glm::rotate( speed_scale*dx, glm::vec3(0,0,1) );
4.鼠標右鍵拖拽,上下移動時攝像機沿視覺座標x軸旋轉,左右移動時攝像機沿世界座標系y軸轉動;
transform_camera *= glm::rotate( speed_scale*dy, glm::vec3(1,0,0) ); glm::vec3 v = glm::vec3( glm::affineInverse(transform_camera)*glm::vec4(0,1,0,0) ); transform_camera *= glm::rotate( -speed_scale*dx, v );
5.鼠標左鍵拖拽,物體按拖拽球旋轉;
void drag_ball(int x1, int y1, int x2, int y2, glm::mat4& Tmodel, glm::mat4& Tcamera) { float r = (float)std::min(win_h, win_w)/3; float r2 = r*0.9f; float ax = x1-(float)win_w/2, ay = y1-(float)win_h/2; float bx = x2-(float)win_w/2, by = y2-(float)win_h/2; float da = std::sqrt(ax*ax+ay*ay), db = std::sqrt(bx*bx+by*by); if(std::max(da,db)>r2){ float dx, dy; if(da>db){ dx = (r2/da-1)*ax; dy = (r2/da-1)*ay; }else{ dx = (r2/db-1)*bx; dy = (r2/db-1)*by; } ax += dx; ay +=dy; bx += dx; by += dy; } float az = std::sqrt( r*r-(ax*ax+ay*ay) ); float bz = std::sqrt( r*r-(bx*bx+by*by) ); glm::vec3 a = glm::vec3(ax,ay,az), b = glm::vec3(bx,by,bz); float theta = std::acos(glm::dot(a,b)/(r*r)); glm::vec3 v2 = glm::cross(a,b); // v2是視覺座標系中的向量,v是v2在物體座標系中的座標 glm::vec3 v = glm::vec3( glm::affineInverse(Tmodel) * Tcamera * glm::vec4(v2[0],v2[1],v2[2],0) ); Tmodel *= glm::rotate( theta*180/3.14f, v ); }
6.鼠標中鍵拖拽,至關於AD鍵和上下鍵;
7.鼠標中鍵滾動,至關於WS鍵。
以上代碼,以可讀性和方便說明原理爲目標,因此實現上不很高效,尤爲是用transform_camera表示攝像機位置和定向而不是視圖矩陣,致使每次都要求transform_camera的逆,能夠利用(AB)-1=B-1A-1等公式進行等價變換提升效率。
6.進階,變換的插值
不少時候,咱們但願對變換進行插值,好比,指定物體在開始和結束兩個時刻的位置和定向(即物體的transformation),但願在這兩個時間點的中間時刻物體可以平滑的變換,從而實現關鍵幀動畫,再好比,咱們指定開始和結束兩個時刻的攝像機的transformation,但願攝像機的transformation可以被插值,從而實現視角的平滑變化。這個問題能夠歸結爲T(0)=Tbegin, T(1)=Tend,求T(t), 0<t<1,使得T(t)隨着t平滑變化,這個問題並不像想象中那麼簡單,T(t)=tTbegin+(1-t)Tend這個函數並不能作到定向(旋轉)的平滑變化,甚至都作不到保持物體形狀不變(剛體變換)。解決方法涉及高深的數學知識,如矩陣的指數和對數,甚至是羣論和李代數,請參考文獻[1]。
源程序下載:連接http://pan.baidu.com/s/1hqrG98K 密碼: jmc5
PPT下載(若是下載後顯示要修復,請右鍵文件,屬性,點下面解除鎖定按鈕):連接http://pan.baidu.com/s/1c0lJigw 密碼: isds
參考文獻