基於 OpenGL 進行 3D 圖形開發
作者:
劉鵬
日期:
2010-05-10
本文介紹了基於 OpenGL 進行 3D 開發的基本思路,協助初學者快速理清思路。 3D 開發的一般思路
3D 開發主要分為三個步驟:建模、渲染、邏輯控制。
建模,指通過一些基本圖元如點、線、三角形、多邊形將物體畫出來,一般使用
3DMax、MilkShape 等建模工具來完成。具體來說,建模涉及模型的構建、貼紋
理、製作動畫等。
渲染,即使用 OpenGL 圖形介面將模型在電腦上畫出來。
邏輯控制,若要模型動起來,需要根據時間計算模型各個頂點的座標,這個通過
程式邏輯來控制。
在 3D 開發中有一個常用的速度指標,即幀每秒(f/s),其計算方法如下:
fps = numFrame / Interval
- numFrame 是當前的 frame 總和,從程式運行起,每運行一個 frame 這個就加1;
- Interval 指從程式運行起的時間總和。
建模
用 3dMax 等工具建模的一般思路是:
- 用基本形狀,如三角形、多邊形、曲面、球體、立方體等,把物體輪廓構件出來
- 通過展開、調整頂點等方法對細節進行處理
建模工具有很多,如 3DMax、Maya、Blender、MilkShape、AC3D 等,其中
MilkShape 短小精悍,容易上手比較推薦新手使用。
Milkshape :建模、貼紋理、加骨骼、做動畫
- mode: 做模型
- group: 構成模型的各個子部分
- material:往各個子部分上貼材質
- joint:骨骼結點,現在還沒想清楚這些 joint 的作用是什麼
- animation:設定關鍵禎,產生動畫
- 產生的模型格式是 .ms3d
Milkshape 的教程可在這裡
下載。
儲存模型有很多種格式,如 md2、ms3d、3ds、obj等。
- md2 速度快,但資料量大,典型的以空間換時間; ms3d 儲存的資料量小,但需要的計算比較多
- md2 的動畫可能不如 ms3d 細膩
OpenGL based Graphics
It's a state machine - Setup the stage, lighting, actors... Then draw it!
使用 OpenGL 進行渲染的總體思路
- clear the screen:glClear (XXX | XXX)
- Reset the view:glLoadIdentity
- move / rotate axis:glTranslatef (x, y, z), glRotatef ()
- draw scene: glBegin, ..., glEnd
- glDrawArray/glDrawElements 完成點、線、多邊形等基本圖元的繪製
貼紋理產生紋理
OpenGL 中的紋理通過一個唯一號引用,通過函數 glBindTexture() 實現。你
可以自己指定這個唯一號,或者通過調用 glGenTextures () 函數產生一個唯一
號。
GLuint texture[1];
glGenTextures(1, &texture[0]);
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexImage2D(GL_TEXTURE_2D, 0, 3, sizeX, sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, pData);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
此時 pdata 裡的資料就可以釋放了
紋理管理器
在實際開發時,往往需要一個紋理管理器提供紋理的裝載、釋放、擷取等功能,
下面的文章介紹了一個紋理管理器的設計和實現。
A Singleton Texture Manager for OpenGL
如何貼紋理
以片來自:http://blogs.msdn.com/danlehen/
核心繪製代碼如下所示:
glVertexPointer(3, GL_FLOAT, 0, m_vertices);
glNormalPointer(GL_FLOAT, 0, m_normals);
glBindTexture(GL_TEXTURE_2D, m_textureId);
glTexCoordPointer(2, GL_FLOAT, 0, m_uvs);
glDrawElements(GL_TRIANGLES, m_triangleNums * 3, GL_UNSIGNED_SHORT,m_indices);
或者
glDrawArrays(GL_TRIANGLES, 0, m_triangleNums * 3);
在嵌入式裝置上的用 OpenGL ES 貼紋理,以 Androiod 為例
在 android 平台上,紋理圖的長寬必須是 2 的冪,紋理圖尺寸超過 512x512
時 fps 會顯著降低。一般的做法是將所有紋理組合到成一張 256x256 或者
512x512 的大圖。
使用紋理有兩種方法,一是直接用建模工具映射好,二是通過動態調整紋理座標
來擷取需要的紋理。前者不做介紹,重點介紹下後者。
調整紋理座標的方法有兩種:一是手工修改現有紋理代碼,二是通過
glMatrixMode (glTexture) 對紋理座標進行操作。
glMatrixMode (glTexture) 調整紋理座標的代碼如下:
// move texture
glMatrixMode(GL_TEXTURE);
glPushMatrix();
glLoadIdentity();
// move the texture - this does not work
glTranslatef(offset, 0.0f, 0.0f);
glTranslatef(0.0f, offset, 0.0f);
glTranslatef(0.0f, 0.0f, offset);
glPopMatrix();
注意:
- 紋理座標的位移值範圍為:-1~1
- 紋理座標的原點在紋理圖的左下角,若沿 Y 軸向下位移,位移量為負
- glMatrixMode (GL_TEXTURE)往往與 glMatrixMode (GL_MODELVIEW)混用,注意儲存好現場。
gluPerspective 和 gluLookAt 1
函數原型如下所示:
gluLookAt(GLdoble eyex,GLdouble eyey,GLdouble eyez,
GLdouble centerx,GLdouble centery,GLdouble centerz,
GLdouble upx,GLdouble upy,GLdouble upz);
gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar);
gluPerspective
首先得設定 gluPerspective,來看看它的參數都表示什麼意思:
- fovy,可以理解成眼睛睜開的角度,即,視角的大小,如果設定為0,相當你閉上眼睛了,所以什麼也看不到,如果為180,那麼可以認為你的視界很廣闊;
- aspect,實際視窗的縱橫比,即x/y;
- zNear,表示你近處的裁面;
- zFar,表示遠處的裁面。
我們知道,遠處的東西看起來要小一些,近處的東西看起來會大一些,這就是透視
(perspective)原理,如所示:
假設那兩條線表示公路,理論上講,它們的兩條邊是平行的,但現實情況中,它
們在遠方(可以無限遠)總要相交於一點,實際線段 AB 的長度等於 CD 的長度,
只是在此例中使用了透視角,故會有如上的效果,是不是很接近現實的情況?
結合我們剛才這兩個函數:
- zNear,眼睛距離近處的距離,假設為10米遠,請不要設定為負值,OpenGl 就傻了,不知道怎麼算了;
- zFar,表示遠處的裁面,假設為1000米遠;
就是這兩個參數的意義了。
再解釋下那個"眼睛睜開的角度"是什麼意思,首先假設我們現在距離物體有50個
單位距離遠的位置,在眼睛睜開角度設定為 45 時,請看大螢幕:
我們可以看到,在遠處一個球;現在我們將眼睛再張開點看,將“眼睛睜開的角
度”設定為 178 (180度表示平角,那時候我們將什麼也看不到,眼睛睜太大了,眼
大無神)。
我們只看到一個點,因為我們看的範圍太大了,這個球本身大小沒有改變,但是
它在我們的“視界”內太小了。
反之,我們將眼睛閉小些,改為 1 度看看會出現什麼情況呢?
在我們距離該物體 3000 距離遠,“眼睛睜開的角度”為 1 時,我們似乎走進了
這個球內,這個是不是類似於相機的焦距?
當我們將“透視角”設定為 0 時,我們相當於閉上雙眼,這個世界清靜了,我們
什麼也看不到了。
gluLookAt
現在來看 gluLookAt 函數。
它共接受三對座標,分別為 eye,center,up。
- eye, 表示我們眼睛在“全局座標系”中的位置;
- center, 表示眼睛“看”的那個點的座標;
- up,表示觀察者本身的方向。
如果將觀察點比喻成我們的眼睛,那麼這個 up 則表示我們是正立還是倒立。從
不同角度看,所看的影像大不相同。若需要指明我們現在正立,那麼 X,Z 軸為
0,Y 軸為正即可,通常將其設定為 1,只要表示一個向上的向量(方向)即可。
我們指定 0.1f 或 0.00001f 或 1000.0f,效果是一樣的,只要能表示方向即可。
球是畫在全局座標系的原點上的,即 O(0,0,0) 座標上,我們的眼睛位於觀察點
A(0,0,100),Z 軸向螢幕裡看去的方向為負,螢幕外我們的位置,Z 軸為正值,
其實很好理解,即我們距離原點的距離,設定 100,將觀察到如所示的影像。
如果我們向前或向後移動,則相應的映像會變大或變小,這裡其實就是運用了透
視原理,近處的物體大,遠處的物體小,實際物體的大小是不變的。
同理改變 center 座標(眼睛看去的那個點,可簡單理解為視線的終點)也會影響
球的大小,同樣可以認為是改變了物體與觀察點的距離所致。
測試
以上理解了之後,來做一個測試。
透視圖不變,最遠處仍為 3000,近處為 0.1。
gluPerspective // 設定透視圖
(45, // 透視角設定為 45 度,在Y方向上以角度為單位的視野
(GLfloat)x/(GLfloat)y, // 視窗的寬與高比
0.1f, // 視野透視深度:近點1.0f
3000.0f // 視野透視深度:始點0.1f遠點1000.0f
);
將我們的觀察點置於 A(0,10,0),將觀察位置(視線終點)座標置於(0,0,0),然
後在原點開始繪圖,畫一個 V 字形,並將 Z 軸的值從 -1000 遞增加到 +1000,
增量為10,代碼如下:
glColor3f(0.5f, 0.7f, 1.0f);
glBegin(GL_LINES);
for(int i=-1000;i<=1000;i+=10)
{
glVertex3f(0,0,i);
glVertex3f(10,10,i);
glVertex3f(0,0,i);
glVertex3f(-10,10,i);
}
glEnd();
如下所示:
OpenGL 的座標系統變換
OpenGL 的重要功能之一是將三維的全局座標經過變換、投影等計算,最終算出它在顯示裝置上對應的位置。
每次計算螢幕輸出時,OpenGL 採取這樣的方式進行:
螢幕座標點 = 3D 模型點 * 幾何變換棧矩陣(n...1) * 投影變換棧矩陣 (n...1)。其中 n...1 表示從棧頂到棧底。
更複雜的流程是:
螢幕座標點 = 3D 模型座標 -〉 (幾何變換矩陣) -〉 人眼座標 -〉(投影變換矩陣) -〉 正則裝置座標(相對於 OpenGL 螢幕座標原點)
-〉 校正成視窗座標(相對於視窗座標)
簡單的講,OpenGL 中從三維情境到螢幕圖形要經曆如下所示的變換過程:
模型座標-〉全局座標-〉觀察座標-〉投影座標-〉裝置座標
其中四種座標經常要在程式中用到:物體座標(也叫模型座標、局部座標),全局座標,眼座標(也叫觀察座標)和裝置座標.
物體座標:
以物體某一點為原點而建立的“全局座標”,該座標系僅對該物體適用,用來簡化對物體各部分座標的描述。物體放到情境中時,各部分經曆的座標變換相同,相對位置不變,所以可視為一個整體,與人類的思維習慣一致;
全局座標:
是OpenGL中用來描述情境的座標,Z+軸垂直螢幕向外,X+從左至右,Y+軸從下到上,是右手笛卡爾座標系統。我們用這個座標系來描述物體及光
源的位置。 OpenGL 中有一個座標轉換矩陣棧 (Modeview),棧頂就是當前座標變換矩陣,進入 OpenGL 管道的每個座標
(齊次座標)都會乘上這個矩陣,結果才是對應點在情境中的全局座標。將物體放到情境中也就是將物體平移到特定位置、旋轉一定角度,這些操作都是座標變換。
眼座標:
是以視點為原點,以視線的方向為Z+軸正方向的座標系中的方向。OpenGL管道會將全局座標先變換到眼座標,然後進行裁剪,只有在視線範圍(視見
體)之內的情境才會進入下一階段的計算。 OpenGL 有個投影變換矩陣棧
(Projection),棧頂矩陣就是當前投影變換矩陣,負責將情境各座標變換到眼座標。由於所得到的結果是裁剪後的情境部分,稱為裁剪座標。
裝置座標:
OpenGL的重要功能之一就是將三維的全局座標經過變換、投影等計算,最終算出它在顯示裝置上對應的位置,這個位置就稱為裝置座標。在螢幕、印表機等裝置上的座標是二維座標。
幾何變換:平移、旋轉、縮放
- glTranslatef (x, y, z)
- glRotatef (alpha, x, y, z);
- glScalef (x, y, z);
幾何變換棧:為了維護幾何變換, OpenGL 維護一個幾何變換棧,每次幾何變換,
都用相應的矩陣乘對應的棧頂元素,並替換掉上次的棧頂。
對於幾何變換棧,有兩個操作可以使用:
- glPushMatrix(): 儲存當前座標系 ,複製當前棧頂,並把複製的內容再次放到棧頂,能夠保護棧上以前的內容;
- glPopMatrix(): 恢複當前座標系。
投影變換 : 正交投影 (正射投影、平行投影),透視投影
- 正交投影:將 3D 模型平行的映射到平面上,glOrtho(xleft, xright, ybottom, ytop, znear, zfar);
- 透視投影:將 3D 模型映射到相對某個觀察點的平面上, gluPerspective(fovy, aspect, znear, zfar);
深入探討透視投影變換:http://www.cppblog.com/zmj/archive/2008/08/28/58936.html
glOrtho 函數的意義: http://flyingliang.spaces.live.com/blog/cns!9112491EC93C817B!1228.entry
正交投影的特點:
無論物體距離相機多遠,投影后的物體大小尺寸不變。這種投影通常用在建築藍圖繪製和電腦輔助設計等方面,這些行業要求投影后的物體尺寸及相互間的角度不變,以便施工或製造時物體比例大小正確。
透視投影的特點:離觀察者越遠的對象越小。
切換當前棧
- 切換當前操作的棧為投影變換棧:glMatrixMode(GL_PROJECTION);
- 切換當前操作的棧為幾何變換棧:glMatrixMode(GL_MODELVIEW);
- 清除當前操作棧的內容:glLoadIdentity();
變化觀察模型的角度,可以在切換到幾何變換棧的時候進行:
- glMatrixMode(GL_MODELVIEW); // 模型視圖矩陣
- glLoadIdentity();
- gluLookAt(6.0, 8.0, 10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); // 旋轉視點
一段典型的 OpenGL 代碼
// Init *
glClearColor (0.0f, 0.0f, 0.0f, 0.0f); /* Set background color*/
glClearDepth (1.0);
glShadeModel (GL_SMOOTH);
glMatrixMode (GL_PROJECTION); /* 設定投影變換 */
glLoadIdentity ();
gluPerspective (......);
glMatrixMode (GL_MODEVIEW);
//* 清屏 *
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity ();
glTranslatef (x, y, z); /* 平移 */
glRotatef (angle, x, y, z); /* 旋轉 */
glBegin (GL_POLYGON);
glColor3f (...);
glVertex3f (...);
... ...
glEnd ();
/* ReSizeScene */
glViewport (0, 0, width, height);
glMatrixMode (GL_PROJECTION);
glLoadIdentity();
gluPerspective(... ...);
glMatrixMode(GL_MODELVIEW);
How to walk around and explore the world
- move camera around and draw the 3D environment relative to the camera positon. But it is slow and hard to code.
- Rotate / translate the world in the opposite manner.
- Rotate and translate the camera position according to user commands
- Rotate the world around the origin in the opposite direction of the
camera rotation (giving the illusion that the camera has been rotated)
- Translate the world in the opposite manner that the camera has been
translated (again, giving the illusion that the camera has moved)
Reference
- 終於搞明白 gluPerspective 和 gluLookAt 的關係了