基於 OpenGL 進行 3D 圖形開發

分類: openGL

3D 開發的一般思路

轉自:http://www.linuxgraphics.cn/graphics/opengl_dev_summary.html

3D 開發主要分爲三個步驟:建模、渲染、邏輯控制。

建模,指通過一些基本圖元如點、線、三角形、多邊形將物體畫出來,一般使用 3DMax、MilkShape 等建模工具來完成。具體來說,建模涉及模型的構建、貼紋 理、製作動畫等。

渲染,即使用 OpenGL 圖形接口將模型在計算機上畫出來。

邏輯控制,若要模型動起來,需要根據時間計算模型各個頂點的座標,這個通過 程序邏輯來控制。

在 3D 開發中有一個常用的速度指標,即幀每秒(f/s),其計算方法如下:

fps = numFrame / Interval
  • numFrame 是當前的 frame 總和,從程序運行起,每運行一個 frame 這個就加1;
  • Interval 指從程序運行起的時間總和。

建模

用 3dMax 等工具建模的一般思路是:

  1. 用基本形狀,如三角形、多邊形、曲面、球體、立方體等,把物體輪廓構件出來
  2. 通過拉伸、調整頂點等方法對細節進行處理

建模工具有很多,如 3DMax、Maya、Blender、MilkShape、AC3D 等,其中 MilkShape 短小精悍,容易上手比較推薦新手使用。

Milkshape :建模、貼紋理、加骨骼、做動畫

  1. mode: 做模型
  2. group: 構成模型的各個子部分
  3. material:往各個子部分上貼材質
  4. joint:骨骼結點,現在還沒想清楚這些 joint 的作用是什麼
  5. animation:設置關鍵禎,生成動畫
  6. 生成的模型格式是 .ms3d

Milkshape 的教程可在這裏下載。

保存模型有很多種格式,如 md2、ms3d、3ds、obj等。

  1. md2 速度快,但數據量大,典型的以空間換時間; ms3d 保存的數據量小,但需要的計算比較多
  2. 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.
    1. Rotate and translate the camera position according to user commands
    2. Rotate the world around the origin in the opposite direction of the camera rotation (giving the illusion that the camera has been rotated)
    3. Translate the world in the opposite manner that the camera has been translated (again, giving the illusion that the camera has moved)

Reference

  1. 終於搞明白 gluPerspective 和 gluLookAt 的關係了
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章