3D 開發的一般思路
轉自:http://www.linuxgraphics.cn/graphics/opengl_dev_summary.html3D 開發主要分爲三個步驟:建模、渲染、邏輯控制。
建模,指通過一些基本圖元如點、線、三角形、多邊形將物體畫出來,一般使用 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)