OpenGL坐標變換及其數(shù)學原理,兩種攝像機交互模型(附源程序)
實驗平臺:win7,VS2010
先上結果截圖(文章最后下載程序,解壓后直接運行BIN文件夾下的EXE程序):
a.鼠標拖拽旋轉物體,類似于OGRE中的“OgreBites::CameraStyle::CS_ORBIT”。

b.鍵盤WSAD鍵移動鏡頭,鼠標拖拽改變鏡頭方向,類似于OGRE中的“OgreBites::CameraStyle::CS_FREELOOK”。

1.坐標變換的一個例子,兩種思路理解多個變換的疊加
現(xiàn)在考慮Scale(1,2,1); Transtale(2,1,0); Rotate(pi/4,(0,0,1)); 這3個變換(下文用S, T, R簡寫),作用到原先中心位于原點邊長為2的立方體上的情況。
坐標系顯示說明及變換前的場景如下:


以上變換用OpenGL(經(jīng)典管線)和GLM實現(xiàn)代碼分別如下:
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();
變換后的場景如下圖:

現(xiàn)在,可以用兩種思路來理解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);相對于全局坐標系的原點,而不是物體中心,所以物體的中心發(fā)生了變化。思路2物體坐標系變換(右圖),第2步T(2,1,0);以物體坐標系為參考,因為物體坐標系的Y軸在上次變換中被拉長了,所以Y軸的1長度也被拉長了,第3步R(pi/4,(0,0,1));也以物體坐標系為參考,因為物體坐標系的Y軸被拉長了,頂點的旋轉軌跡在我們看來是個橢圓,如圖中青色所示,同樣,所旋轉的45度在我們看來也不是45度(物體坐標系并不知道這一點,它只根據(jù)當前坐標系進行變換,并且總是覺得自己的旋轉軌跡是圓,角度是45度)。
這兩種思路顯然不是巧合,它們的背后有深刻的數(shù)學原理,請接著看下一節(jié)。
2.坐標變換的數(shù)學原理,兩種思路背后的數(shù)學解釋
因為涉及好多數(shù)學公式,這里采用一種新的撰文形式,即PPT加講解的形式,每頁PPT截圖后面有旁白解釋。如果嫌截圖不清楚,文章最后給出了PPT的下載鏈接。

數(shù)域一般取實數(shù)集或復數(shù)集。基是一組線性無關的向量組,而不一定是互相正交(垂直)的。坐標用列向量表示,在OpenGL中點的坐標也用列向量表示。

基的定義,空間中任一向量均能用基線性表示,另一組基中的向量也如此。T為n介方陣。注意這里T是乘在右邊。

Y的公式同理直接寫出來了。X, Y均為列向量。注意這里T乘在Y的左邊,因為Y是列向量嘛,對比前一頁PPT,基變換公式中T乘在左邊,這種差異是關鍵,且往下看。目前講到的基變換與坐標變換均為線性變換(符合f(ax+by)=af(x)+bf(y)的稱為線性),線性變換將原點變換為原點,而OpenGL中的變換可以平移,下面講到,這是仿射變換。

R表示實數(shù)集合,|A|表示A的行列式(determinant,有時也表示為det(A))。限制A的行列式不為0是要求A非奇異(not singular, invertible,可逆),因為A不可逆時可能將直線映射為一點,即將n維空間壓縮為小于n維。另外A的行列式如果為負則變換產生鏡像(如將右手系變換為左手系)。之前用X,Y表示坐標也即列向量,現(xiàn)在用粗體小寫字母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時要將所有坐標都縮放一個倍數(shù)使之為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,那么變換可能將直線變?yōu)榍€(請自行舉例),即變換后的坐標是原坐標的有理分式(這在OpenGL投影矩陣中被應用)。

原基為向量,前n個元素是齊次坐標系中的向量,這里將線性變換(沒有平移)推廣到仿射變換,即加入原點,原點是新基中唯一的點(其他為向量)。

這里將之前用的字母T改用A,現(xiàn)在的T表示齊次坐標下的變換矩陣(見PPT第2頁)。注意b其實是第一組基下的坐標(和A一樣),這組坐標和基相乘得到它表示的向量。這里再次注意T在基變換和坐標變換公式中的位置。強調一下,T并不是自由的n+1介方陣,它的最后一行固定為n個0接1個1。第n+1個坐標w為0時表示的向量(認為是兩個點的差),向量的仿射變換可以看成是其兩個端點仿射變換后做差,這時T的平移部分將被抵消(請看下一節(jié)仿射變換的分解),也就是說w為0的向量的仿射變換只和T的旋轉和縮放部分有關(也就是自由向量的概念,向量只有方向和大小,沒有起點)。


這里順便提一下,矩陣相乘的幾何意義就是變換的疊加,即線性映射的疊加。注意一個細節(jié),這里的每個T既表示仿射變換本身,又表示仿射變換的變換矩陣,并沒有加以區(qū)分,這是合理的,因為仿射變換和仿射變換矩陣之間有一一對應的關系(所有仿射變換構成的空間和所有仿射變換矩陣構成的空間同構)。至此徹底了解了兩種思路的數(shù)學原理。再次強調這里的仿射變換T可能不一定是剛體變換,它有可能產生縮放、錯切變形。第1節(jié)的例子就不是剛體變換,以上的兩種思路和解釋是對仿射變換成立的,不限于剛體變換(旋轉和平移或其疊加)。
3.更深入的數(shù)學,坐標變換的分解(矩陣的分解)
接著用PPT的形式~

這里都講的是三維空間。I表示單位矩陣(identity matrix,數(shù)學書中一般用E表示)。||v||表示范數(shù),在向量空間中也就是向量的模長。注意到 (u·x)u=(uuT)x,u×x(叉乘)等于 u波浪線 矩陣乘 x,旋轉公式只要選定 u, u×x, x-(u·x)u 三個新基就很好看懂了(文獻[1]第11頁)。這里既用字母T表示仿射變換矩陣,又用其表示平移矩陣,T的具體含義可以根據(jù)上下文區(qū)分不會混淆,用C++術語來說,它們的參數(shù)列表不同。旋轉矩陣沿xyz軸的特殊形式請自行將v設為特殊值進行推導。那現(xiàn)在的問題是,任意給一個仿射變換矩陣T(要符合最后一行是n個0接1個1),T能否分解為T(x,y,z), S(x,y,z), R(a,(x,y,z))的組合(連乘積)呢?答案是肯定的,請繼續(xù)往下看。

再次,既用T, R, S表示矩陣,又用其表示平移、旋轉、縮放函數(shù),請很據(jù)上下文區(qū)分。行列式為正的正交矩陣是一個旋轉矩陣,對稱矩陣是個縮放矩陣(縮放值可能有負值,這時產生鏡像,即手性變化),經(jīng)過對角化后分解為旋轉矩陣和沿xyz軸縮放的矩陣(即對角陣)。注意極式分解具有唯一性,對角化不具有唯一性,但不唯一性也僅限于調換對角陣的行或列(相應調換對角陣兩邊的旋轉矩陣)。可以根據(jù)T, S, R(平移、縮放、旋轉)的逆來構造整個變換T的逆((AB)-1=B-1A-1,當然也可以不分解直接求逆矩陣)。
4.圖形學中的變換模型以及OpenGL的實現(xiàn)
這里講的變換模型是指一種“思維模型”,也就是說用這個模型去思考可以很方便對物體位置和定向進行操作,而具體的實現(xiàn)能夠保證按照這個模型思考一定能夠得到正確答案,但這個實現(xiàn)可能根本就不是按部就班的按照模型實現(xiàn)具體坐標的計算,所以還要講OpenGL的實現(xiàn)。
圖形學中的變換模型一般涉及物體坐標系(model space)、世界坐標系(world space)、視覺坐標系(eye space)、規(guī)范化設備坐標系(normalized device space)、窗口像素坐標系(window space),這些坐標系中的坐標相應叫做某某坐標,如世界坐標系中的坐標叫做世界坐標(world coordinates)。一個示意圖如下(用Blender軟件制作和渲染的):

如圖中所標注的,猴頭上面的坐標框架表示物體坐標系;那個最大的坐標框架是世界坐標系,水紅色的是地板;黑色的攝像機上的是視覺坐標系,視覺坐標系的定義是,鏡頭所指方向為z負方向,攝像機正上為y正方向,右手法則確定x方向。
坐標的變換如下(請見文獻[8]第66頁):

1.模型變換,視圖變換
現(xiàn)在舉例子說明物體坐標到視覺坐標的變換,場景是(請看上面猴頭那個圖),猴頭的中心位于物體坐標系原點,猴頭中心位于世界坐標系的(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函數(shù)中的LookAt函數(shù)返回的變換矩陣是,將攝像機設置為函數(shù)參數(shù)指定的情況所需要的視圖矩陣。
2.投影變換
投影變換請見下圖(摘自文獻[4]):

投影變換后進入坐標裁剪,即落在紅色方框外的部分將被裁剪掉。
3.透視除法
投影變換后齊次坐標的第4個分量w可能不為1,透視除法即將xyz分量都除以w,得到規(guī)范化設備坐標(特點是xyz分量范圍在-1到+1之間),對透視投影而言,這一步是非線性的(遠處物體被壓縮)。如下圖(摘自文獻[4]):


投影變換和透視除法合起來的效果是,將指定的視景體(也叫平截頭體,也就是那個裁剪框)變換為邊平行于xyz軸且xyz范圍都是-1到+1中心位于原點的正方體。注意z坐標的符號變化。如下圖(摘自文獻[7]):


4.視口變換
再經(jīng)過視口變換,即調用OpenGL的glViewport函數(shù),對應到窗口像素,注意,在OpenGL中,像素坐標系的原點位于左下角,向右為x軸正向上為y軸正(而一般圖片像素都是以左上角為原點)。具體來說,視口變換將規(guī)范化設備坐標的位于[-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實現(xiàn)
具體到OpenGL的實現(xiàn),OpenGL和數(shù)學中相同采用右手系,OpenGL把模型變換和視圖變換合二為一,即模型視圖矩陣。OpenGL和GLM的變換矩陣都是按照列優(yōu)先存儲在內存中,這和C++二維數(shù)組不同,其實,GLM中的4x4矩陣是由4個列向量組成的。按照上面的分析,當OpenGL的模型視圖矩陣和投影矩陣均為單位陣時,這時攝像機位于世界坐標系原點看向z負方向,向右方向沿x軸正方向,向上方向沿y正方向,由于投影矩陣為單位陣,這時為正交投影(另一種是透視投影),裁剪面為xyz的±1,也就是說,對應到最后的顯示窗口,x方向向右,y方向向上,z方向垂直屏幕向外,窗口中心對應坐標原點,窗口邊緣對應±1,并且z值小的片斷遮擋z值大的片斷(正好和離攝像機的遠近關系反了,這是因為沒有對z坐標進行變號)。對了,OpenGL除了模型視圖矩陣和投影矩陣之外,還有文理坐標變換矩陣和顏色變換矩陣。請見OpenGL官方手冊文獻[8]2.12和2.16。
5.兩種攝像機交互模型
現(xiàn)在用前面的知識實現(xiàn)兩種最常見的攝像機交互模型,先說下對上面說的變換模型的實現(xiàn),程序有如下全局變量:
glm::mat4 transform_camera(1.0f); // 攝像機的位置和定向,即攝像機在世界坐標系中位置 glm::mat4 transform_model(1.0f); // 模型變換矩陣,即物體坐標到世界坐標 glm::vec4 position_light0(0); // 光源位置,世界坐標系中的坐標 float speed_scale=0.1f; // 鼠標交互,移動速度縮放值
在繪制函數(shù)中,這些全局變量被應用如下(第一行之所以求逆,是因為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軟件制作):

數(shù)學描述如下,圖中的 坐標系是視覺坐標系,對應到屏幕也就是向右為x軸,向上為y軸,垂直屏幕向外為z軸:

推導旋轉矩陣如下:

這里都假設 a, b 兩點在距離屏幕中心小于r的情況,如果大于r(其實是大于水紅色圓半徑)請見上圖的中的 p 點,用 p撇 代替 p 點,水紅色圓半徑小于r是希望鼠標在距離中心大于r的地方沿徑向移動物體也能產生旋轉。OpenGL官方Wiki上有個更好的解決方法,見文獻[9]。
第二種漫游模型(姑且叫漫游吧),要求按下鍵盤WSAD鍵攝像機前進、后退、左移、右移,鼠標左右移動時攝像機鏡頭左右掃動,鼠標上下移動時攝像機鏡頭做俯仰動,如下圖所示:

注意鼠標左右移動的旋轉軸要沿世界坐標系的y軸旋轉,而不是攝像機自己的y軸,以防止視角傾斜,鼠標上下移動就沿攝像機自己的x軸旋轉就行。鍵盤WSAD鍵沿攝像機的z軸和x軸移動就行。
下面是程序實現(xiàn),程序中的鍵盤和鼠標響應如下:
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鍵。
以上代碼,以可讀性和方便說明原理為目標,所以實現(xiàn)上不很高效,尤其是用transform_camera表示攝像機位置和定向而不是視圖矩陣,導致每次都要求transform_camera的逆,可以利用(AB)-1=B-1A-1等公式進行等價變換提高效率。
6.進階,變換的插值
很多時候,我們希望對變換進行插值,比如,指定物體在開始和結束兩個時刻的位置和定向(即物體的transformation),希望在這兩個時間點的中間時刻物體能夠平滑的變換,從而實現(xiàn)關鍵幀動畫,再比如,我們指定開始和結束兩個時刻的攝像機的transformation,希望攝像機的transformation能夠被插值,從而實現(xiàn)視角的平滑變化。這個問題可以歸結為T(0)=Tbegin, T(1)=Tend,求T(t), 0<t<1,使得T(t)隨著t平滑變化,這個問題并不像想象中那么簡單,T(t)=tTbegin+(1-t)Tend這個函數(shù)并不能做到定向(旋轉)的平滑變化,甚至都做不到保持物體形狀不變(剛體變換)。解決方法涉及高深的數(shù)學知識,如矩陣的指數(shù)和對數(shù),甚至是群論和李代數(shù),請參考文獻[1]。
源程序下載:鏈接http://pan.baidu.com/s/1hqrG98K 密碼: jmc5
PPT下載(如果下載后顯示要修復,請右鍵文件,屬性,點下面解除鎖定按鈕):鏈接http://pan.baidu.com/s/1c0lJigw 密碼: isds
參考文獻
- Ochiai, H. and Anjyo, K., Mathematical basics of motion and deformation in computer graphics. in ACM SIGGRAPH 2014 Courses, (Vancouver, Canada, 2014), ACM, 1-47.(到ACM網(wǎng)站下載,可能需要大學IP);
- 《高等代數(shù)簡明教程》(上冊,第二版,藍以中編著,北京大學出版社,2007),第4章(到當當網(wǎng)買);
- Angle E, Shreiner D. Interactive Computer Graphics: A Top—down Approach with Shader-based OpenGL. 2011. Chapter 3(到亞馬遜買);
- http://www.opengl-tutorial.org/, Tutorial 3.
- OpenGL Mathematics (GLM), Open source library.
- 《OpenGL編程指南》(原書第7版,Dave Shreiner等著,李軍等譯,機械工業(yè)出版社,2011),第3章(到當當網(wǎng)買);
- University of Freiburg的Computer Graphics小組課程主頁(去課程主頁,圖形管線PPT);
- OpenGL 3.3 Compatibility Profile Specification (updated March 11, 2010), 2.12, 2.16(去官網(wǎng)下載);
- https://www.opengl.org/wiki/Trackball。
posted on 2014-11-12 17:35 liangliangh 閱讀(12253) 評論(6) 收藏 舉報
浙公網(wǎng)安備 33010602011771號