1. 程式人生 > >3D物體建模和渲染的跟蹤球互動設計和實現

3D物體建模和渲染的跟蹤球互動設計和實現

1. 前言
整理電腦時發現自己本科選了一些有意思的選修課,寫了一些小專案,馬上就要畢業了,不想就這麼刪了,決定花點時間整理出來,跟大家分享一下。
2. 實驗目的
綜合應用計算機圖形學老師所教授的理論知識,本實驗採用openGL程式設計,實現一個基於3D物體建模和渲染的跟蹤球互動應用程式,應用程式主要實現以下功能:
① 畫出兩個圖形,其中一個是可以遞迴細分的球面,一個是立方體;
① 在WC座標原點處新增一個光源,物體表面將會有光照的顏色;
② 除此之外,程式能夠給物體表面貼圖,因此物體表面還混有紋理貼圖的顏色;
程式還應能夠對物體進行互動,包括:
③ 鍵盤互動,能夠用鍵盤的0~8控制球面的遞迴細分的次數。用鍵盤+,-控制物體透明程度,A在0..1之間變化,其中初始為不透明;
④ 滑鼠互動,程式採用模擬跟蹤球互動技術實現景物觀察,通過控制滑鼠左鍵、中鍵、右鍵進而控制物體的拖動旋轉、轉動、投影變換。
通過此實驗,我們將實現以上功能,並且鞏固掌握的知識和增強應用的能力。本文件的主要目的是設計說明跟蹤球的建模與渲染及其互動的實現。
3. 實驗環境


Windows 7/8/10
Visual Studio 2013 C++
4. 實驗內容
實驗內容分為以下三個方面:
4.1 建模
要在計算機中表示一個三維物體,首先要有它的幾何模型表達。因此,三維模型的建模是計算機圖形學的基礎,是其他內容的前提。程式的第一步是確定基本圖形從而進行建模。在本實驗中,首先設計兩個3D圖形,一個是能夠細分的球面,一個是立方體。
4.1.1 細分球面
對於球面是由多個四面體組成的,所以首先初始化四面體,定義一個長度為4的point矩陣表示四面體的頂點座標:

/初始化四面體
point v[] = { { 0.0, 0.0, 1.0 }, { 0.0, 0.942809, -0.33333 }
, { -0.816497, -0.471405, -0.333333 }, { 0.816497, -0.471405, -0.333333 } };

為了實現細分,可以將三角形的細分應用於四面體的每一個面,所以一共需要呼叫四次細分函式divide_triangle。函式divide_triangle是一個遞迴函式,使用頂點編號右手規則應用於建立向外指向的面,將四面體面的相鄰頂點向量相加、歸一化,再用處理後的頂點與相鄰的兩個頂點作為引數呼叫函式本身以實現進一步的細分,在遞迴結束部分呼叫函式triangle畫出三角形。從而,四面體的每一面實現了細分,進而實現了球面的細分。其中divide_triangle的核心演算法如下:

 if (m>0)
    {
        for (j = 0; j<3; j++) v1[j] = a[j] + b[j];
        normal(v1);
        for (j = 0; j<3; j++) v2[j] = a[j] + c[j];
        normal(v2);
        for (j = 0; j<3; j++) v3[j] = b[j] + c[j];
        normal(v3);
        divide_triangle(a, v1, v2, m - 1);
        divide_triangle(c, v2, v3, m - 1);
        divide_triangle(b, v3, v1, m - 1);
        divide_triangle(v1, v3, v2, m - 1);
    }
    else(triangle(a, b, c)); /* draw triangle at end of recursion */

4.1.2 立方體
對於立方體,首先定義一個長度為8的二維的頂點陣列vertices來表示立方體的頂點座標,使用了浮點數座標:

//立方體點
GLfloat vertices[][3] = {
    { -0.5, -0.5, -0.5 }, { 0.5, -0.5, -0.5 }, 
    { 0.5, 0.5, -0.5 }, { -0.5, 0.5, -0.5 },
    { -0.5, -0.5, 0.5 }, { 0.5, -0.5, 0.5 }, 
    { 0.5, 0.5, 0.5 }, { -0.5, 0.5, 0.5 }
};

接著需定義立方體物件的六個面,加入顏色描述和其他引數,為此在函式DrawCube分別六次呼叫glBegin(GL_QUADS),必須明確每一個面的頂點順序符合從立方體外部對其觀察時為逆時針次序的要求。而此實驗的立方體表面帶有紋理貼圖,將在下一部分進行設計實現討論。
4.2 渲染
有了三維模型或場景,為了把這些三維幾何模型畫出來以產生令人賞心悅目的真實感影象,將應用到多種渲染技術。在本實驗中,將採用光照和紋理貼圖。
4.2.1 光照
本實驗在WC座標原點處新增一個光源GL_LIGHT0,首先指定一個光源的位置和型別。光源型別和光源位置座標用一個四元素的浮點數向量來指定,該向量的前三個元素給出世界座標位置,因為我們要設在原點,所以前三個元素皆為0.0;本實驗中對該位置向量的第四個元素賦值為1.0,即設定的光源是一個區域性光源,光源位置被光照子程式用來確定對場景中每一個物件的光照方向。最後,利用glLightfv(GL_LIGHT0, GL_POSITION, sun_light_position)指定了一個位置在原點的區域性光源。
還應指定光源的顏色,我們使用符號顏色特性常量GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR來指定場景環境光、漫反射光照、鏡面反射光照的顏色。其中每一個通過指定四元素浮點值集來賦值,分別為RGBA。在本實驗中,通過以下程式碼:


    //設定光源,光源顏色為紅色
    GLfloat light_ambient[] = { 0.0, 0.0, 0.0, 1.0 };
    GLfloat light_diffuse[] = { 1.0, 0.0, 0.0, 1.0 };
    GLfloat light_specular[] = { 1.0, 1.0, 1.0, 1.0 };

    glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);//GL_AMBIENT表示各種光線照射到該材質上,經過很多次反射後最終遺留在環境中的光線強度(顏色)  
    glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);//漫反射後
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);//鏡面反射後
    glLightfv(GL_LIGHT0, GL_POSITION, sun_light_position);//指定第0號光源的位置 

將光源0的預設顏色設定為環境光為黑色,漫反射光為紅色而鏡面反射光為白色。
除了設定光源屬性外,還要指定物體表面的特性,表面的反射係數和其他可選特性可用函式glMaterialfv(surFace, surProperty, propertyValue)設定,並且將引數surFace賦以GL_FRONT_AND_BACK;surProperty用來標識表面引數;propertyValue用來設定相應的值,除了鏡面反射指數外所有其他特性均用向量值指定。本實驗中,surProperty的符號常量有GL_SPECULAR、GL_AMBIENT、GL_DIFFUSE、GL_EMISSION和GL_SHININESS,用來設定鏡面反射係數、環境光係數、漫反射係數、散射顏色和鏡面反射指數:

//設定球體的球面材質
    GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };//鏡面光的反射係數
    GLfloat mat_diffuse[] = { 1.0, 1.0, 1.0, 1.0 };//漫反射的反射係數
    GLfloat mat_ambient[] = { 1.0, 1.0, 1.0, 1.0 };//環境光的反射係數
    GLfloat sun_mat_emission[] = { 0.0f, 0.3f, 1.0f, 1.0f };//發射光顏色
    GLfloat mat_shininess =  30.0 ;//鏡面反射指數

    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, mat_specular);
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, mat_ambient);
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, mat_diffuse);
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, mat_shininess);
    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, sun_mat_emission);

在光源與材質屬性設定完畢之後,應該使用glEnable(GL_LIGHT0)點亮光源0,並用glEnable(GL_LIGHTING)啟用光源。除此,還應使用glShadeModel(GL_SMOOTH)平滑陰影,並且通過glEnable(GL_DEPTH_TEST)啟用深度測試,這樣在後面的物體將會被擋著。
4.2.2 紋理貼圖
本實驗,為立方體表面紋理貼圖,首先定義一個6元素的紋理陣列texture用於存放紋理名。因為立方體有六個表面,所以需要載入表面紋理六次,所以需定義一個長度為6的AUX_RGBImageRec型陣列TextureImage用於存放讀取的圖片。對於一次的載入表面紋理貼圖函式實現如下:
① 首先得將圖片載入,利用TextureImage[0] = auxDIBImageLoad(“a.bmp”)可讀取bmp格式的圖案存放入TextureImage[0]中;
② 使用glGenTextures(1, &texture[0]); glBindTexture(GL_TEXTURE_2D, texture[0]);來申請一個紋理名字來將它用於某個圖案,並激活一個命名的圖案。所以最終,glGenTextures生成了一個紋理名,並存放在了texture[0]。
③ 接下來便是建立二維RGBA紋理空間的引數:

glTexImage2D(GL_TEXTURE_2D, 0,4, TextureImage[0]->sizeX,
TextureImage[0]->sizeY,0, GL_RGB, GL_UNSIGNED_BYTE,
TextureImage[0]->data);

④ 使用glTexParameteri為紋理對映子程式指定引數;
⑤ 重複①~④,建立六個紋理圖案;
⑥ 當一個紋理圖案使用完畢之後,需要刪除它以釋放其在OpenGL的紋理記憶體佔用的空間,通過free(TextureImage[i]->data)來實現;當然最後還得記住釋放影象結構,free(TextureImage[i])。
當載入紋理貼圖之後,就要將這些圖案對映到立方體的表面,首先在每一個表面對映之前啟動2D紋理和選擇紋理:

glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture[0]);// 選擇紋理

在定義立方體的每一個表面都得呼叫glBegin(GL_QUADS)。二維紋理空間的一個座標位置可用這個函式來選擇:glTexCoord2f(sCoord, tCoord),對紋理空間進行規範化從而可用0.0到1.0範圍內的座標值來指定圖案,因而我們可以使用任意紋理座標值來表示一個表面上的圖案:

glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture[0]);// 選擇紋理
glBegin(GL_QUADS);
glNormal3f( 0.0F, 0.0F,-1.0F);  
glTexCoord2f(0.0f, 0.0f); glVertex3fv(vertices[1]); // 紋理和四邊形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3fv(vertices[0]); // 紋理和四邊形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3fv(vertices[3]); // 紋理和四邊形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3fv(vertices[2]); // 紋理和四邊形的左上
glEnd();

其中,glNormal3f()用來設定當前的法向量,這個法向量將被應用到緊接下來的glVertex3fv()所定義的頂點上。但是通常各個頂點的法向是各不相同的,所以我們通常在定義每個頂點之前都為它確定一次法向量。
在六個表面都實現紋理貼圖時,最後記得:

glDisable(GL_TEXTURE_2D);
glFlush();

4.3 控制
通過滑鼠與鍵盤的一些互動,使應用程式完成相應的任務。
4.3.1 滑鼠互動
(1) 滑鼠左鍵
本實驗中,當滑鼠左鍵按下時,物體通過捕捉滑鼠移動來改變物體顯示的角度,並且跟隨滑鼠移動來調整旋轉方向;當鬆開滑鼠左鍵的時候,物體能夠繼續按先前速度和方向繼續轉動。
所以利用glutMouseFunc(myMouse)繫結點選滑鼠的函式myMouse,判斷當滑鼠左鍵按下的時候呼叫函式startMotion開始拖動物體,獲取的x、y座標值作為引數傳遞;判斷當滑鼠左鍵鬆開時,呼叫函式stopMotion開始旋轉物體,獲取的x、y座標值作為引數傳遞。
設定一個全域性變數redrawContinue進行控制物體是否旋轉。
對於函式startMotion,獲取到滑鼠點選位置的座標值x、y,並將redrawContinue設為false,即物體停止旋轉,以待滑鼠移動改變物體顯示角度。所以當滑鼠按下進行拖動的過程中將會呼叫函式trackball_ptov(x, y, winWidth, winHeight, lastPos)以更換物體顯示角度。物體將會跟蹤滑鼠拖動的方向來旋轉,旋轉的函式trackball_ptov如下:

void trackball_ptov(int x, int y, int width, int height, float v[3])
{
    float d, a;

    v[0] = (2.0F*x - width) / width;
    v[1] = (height - 2.0F*y) / height;
    d = (float)sqrt(v[0] * v[0] + v[1] * v[1]);
    v[2] = (float)cos((M_PI / 2.0F) * ((d < 1.0F) ? d : 1.0F));
    a = 1.0F / (float)sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
    v[0] *= a;
    v[1] *= a;
    v[2] *= a;
}

通過glutMotionFunc(mouseMotion)繫結滑鼠移動函式mouseMotion以獲取滑鼠移動方向:

/*通過捕捉滑鼠移動來改變物體顯示的角度*/
void mouseMotion(int x, int y)
{
    float curPos[3], dx, dy, dz;
    trackball_ptov(x, y, winWidth, winHeight, curPos);

    dx = curPos[0] - lastPos[0];
    dy = curPos[1] - lastPos[1];
    dz = curPos[2] - lastPos[2];

    if (dx || dy || dz) {
        angle = 90.0F * sqrt(dx*dx + dy*dy + dz*dz);

        axis[0] = lastPos[1] * curPos[2] - lastPos[2] * curPos[1];
        axis[1] = lastPos[2] * curPos[0] - lastPos[0] * curPos[2];
        axis[2] = lastPos[0] * curPos[1] - lastPos[1] * curPos[0];

        lastPos[0] = curPos[0];
        lastPos[1] = curPos[1];
        lastPos[2] = curPos[2];
    }

    glutPostRedisplay();
}

在滑鼠鬆開的時候,只要將redrawContinue設為true即可讓物體沿著剛滑鼠拖動方向和速度旋轉,因為glutIdleFunc(myidle)在閒置時將會回撥函式myidle使物體繼續旋轉:

/*定義沒有指令輸入時執行的函式*/
void myidle()
{
    if (redrawContinue == true)
    {
        angle = 0.2;

        glutPostRedisplay();
    }

}

(2) 滑鼠中鍵
當滑鼠中鍵按下時,物體停止轉動。很明顯,只要將redrawContinue設為false即可停止物體的旋轉。當然,按住滑鼠中鍵也可以拖動物體改變顯示角度。
(3) 滑鼠右鍵
本實驗在點選滑鼠右鍵將會彈出選單,以供使用者選擇投影方式——“正交”或“透視”。通過以下程式碼建立右鍵選單,並且新增“正交”和“透視”兩個選項:

glutCreateMenu(Draw_menu);   //建立右鍵選單
glutAddMenuEntry("Orthographic", 1);
glutAddMenuEntry("Perspective", 2);
glutAttachMenu(GLUT_RIGHT_BUTTON);

當選擇正交投影時,使用glMatrixMode(GL_PROJECTION)引入投影模式,並通過glOrtho函式為正交投影指定裁剪視窗和近、遠裁剪平面的引數;
當選擇透視投影的時候,使用glMatrixMode(GL_PROJECTION)引入投影模式,並通過glFrustum函式為透視投影指定裁剪視窗和近、遠裁剪平面的引數;
4.3.2 鍵盤互動
(1) 鍵盤0~8
程式通過鍵盤的0~8鍵控制球面的細分次數,通過glutKeyboardFunc(KeyFunction)繫結鍵盤迭代劃分球體的回撥函式,當用戶進行鍵盤操作的時候,將會通過KeyFunction判斷輸入的字元,如果輸入的是‘q’或‘Q’,則直接退出程式,如果輸入的是0~8的數字,程式將會相應的改變細分控制變數n的值,從而呼叫glutPostRedisplay()以新的劃分次數呼叫函式tetrahedron,從而重新顯示球面圖形。
(2) 鍵盤+、-
通過glutSpecialFunc(ControlBlendKeys)繫結盤調節透視度的回撥函式,當用戶用鍵盤輸入+的時候,將使透明度d增加0.2,相應程式碼如下:

if(key == GLUT_KEY_UP || key == GLUT_KEY_RIGHT)
    {
        d += 0.2;
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        LoadGLTextures();            //載入紋理貼圖
        glCullFace(GL_BACK);         //面的背面剔除
        glEnable(GL_CULL_FACE);      //啟用剔除後向面功能
        //glEnable(GL_LIGHT1); //啟用一號光源
        //glEnable(GL_LIGHTING); //開光
        glColor4f(1.0f, 1.0f, 1.0f, d);//顏色0.5 alpha值
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //指定混合函式
        glEnable(GL_BLEND);//啟用透明
        glEnable(GL_COLOR_MATERIAL);
        glEnable(GL_TEXTURE_2D);//啟用紋理2D
    }

當用戶用鍵盤輸入-的時候,將使透明度d減小0.2,相應程式碼如下:

if(key == GLUT_KEY_DOWN || key == GLUT_KEY_LEFT)
    {
        d -= 0.2;
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        LoadGLTextures();            //載入紋理貼圖
        glCullFace(GL_BACK);         //面的背面剔除
        glEnable(GL_CULL_FACE);      //啟用剔除後向面功能
        //glEnable(GL_LIGHT1); //啟用一號光源
        ////glEnable(GL_LIGHTING); //開光
        glColor4f(1.0f, 1.0f, 1.0f, d);//顏色0.5 alpha值
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //指定混合函式
        glEnable(GL_BLEND);//啟用透明
        glEnable(GL_COLOR_MATERIAL);
        glEnable(GL_TEXTURE_2D);//啟用紋理2D
    }

5. 實驗結果
經過物體的建模、渲染和互動設計,我們實現了老師要求的基本功能,包括設計一個能夠細分的球面和一個有紋理貼圖的立方體;新增光源並設定相應引數,使物體表面顏色混合了光照顏色和紋理貼圖顏色;能夠進行簡單的滑鼠與鍵盤互動:通過滑鼠左鍵進行物體的拖動和旋轉方向速度控制;通過滑鼠中鍵控制物體是否旋轉;通過滑鼠右鍵選擇投影方式;通過鍵盤0~8鍵控制球面細分次數;通過鍵盤+、-控制物體透明度。
以下是程式執行效果圖。
細分次數為5的球面,正交投影:
這裡寫圖片描述
細分次數為1的球面,透視投影:
這裡寫圖片描述
透明度效果展示:
這裡寫圖片描述
6. 下載連結
http://download.csdn.net/download/qq_22408539/10186050