轉載自:http://www.cnblogs.com/liangliangh/p/4089582.html
實驗平台:win7,VS2010
先上結果截圖(文章最後下載下傳程式,解壓後直接運作BIN檔案夾下的EXE程式):
a.滑鼠拖拽旋轉物體,類似于OGRE中的“OgreBites::CameraStyle::CS_ORBIT”。

b.鍵盤WSAD鍵移動鏡頭,滑鼠拖拽改變鏡頭方向,類似于OGRE中的“OgreBites::CameraStyle::CS_FREELOOK”。
1.坐标變換的一個例子,兩種思路了解多個變換的疊加
現在考慮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();
變換後的場景如下圖:
現在,可以用兩種思路來了解S(1,2,1); T(2,1,0); R(pi/4,(0,0,1)); 這三個變換的疊加。
全局坐标變換,所有的變換在一個全局的固定的坐标系下進行,所有操作均以這個坐标系為參考,注意縮放相對于原點進行,這時的變換順序和代碼順序正好相反,為 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