1. 程式人生 > >解讀OpenGL ES 2.0繪製一個三角形的步驟

解讀OpenGL ES 2.0繪製一個三角形的步驟

前言:

        從上一篇文章中,我們在win7下面搭建OpenGL ES 2.0開發環境的時候,成功運行了官方程式設計指導中提供的Hello_Triangle這個例子,最後得到的結果就是在視窗中繪製出一個紅色的三角形,接下來我們就開始來解讀這個demo的程式碼和實現過程。

        在此之前,我們先大致瞭解整個繪製過程的基本步驟:

1. 使用EGL建立螢幕上渲染的表面Surface 2. 載入頂點和片元著色器 3. 建立program物件,連線頂點和片元著色器,連結program物件。 4. 設定視口(viewport)。 5. 清除顏色緩衝區。 6. 繪製一個簡單的圖元。 7. 使顏色緩衝區的內容顯示到EGL視窗表面(螢幕)。
        這麼看來繪製一個簡單的三角形也是一件挺複雜的事情,因為OpenGL ES 2.0是完全基於著色器的,這也就是說我們必須載入和繫結合適的著色器才能進行圖形畫面的繪製,這相比於固定管線的OpenGL ES 1.x要複雜得多。

        

        從工程的結構樹我們可以看到esUtil是一個API工程,其中Common目錄下包含了框架原始碼和OpenGL ES 2.0所需的標頭檔案,關於這部分原始碼的具體內容我們在後面的學習中再做了解。這裡我們要分析的是demo工程的原始碼,其實原始碼就只有Hello_Triangle.c這個檔案。

一、main函式:

        閱讀原始碼的習慣當然是從main函式入口開始,那麼我們來看看main函式:

int main ( int argc, char *argv[] )
{
   ESContext esContext;
   UserData  userData;

   esInitContext ( &esContext );
   esContext.userData = &userData;

   esCreateWindow ( &esContext, "Hello Triangle", 320, 240, ES_WINDOW_RGB );
   
   if ( !Init ( &esContext ) )
      return 0;

   esRegisterDrawFunc ( &esContext, Draw );
   
   esMainLoop ( &esContext );
}

        這裡我們首先聲明瞭一個ESContext結構,這個是框架提供的一個結構體,用於儲存一些公用的資料,這樣就可以不自行去定義全域性變數,當然這對於多平臺的可移植性來說是必須的,因為在某些平臺中是不支援全域性變數的,例如:Symbian。這裡我們可以直接在Common/esUtil.c中找到ESContext的定義:

typedef struct
{
   /// Put your user data here...
   void*       userData;

   /// Window width
   GLint       width;

   /// Window height
   GLint       height;

   /// Window handle
   EGLNativeWindowType  hWnd;

   /// EGL display
   EGLDisplay  eglDisplay;
      
   /// EGL context
   EGLContext  eglContext;

   /// EGL surface
   EGLSurface  eglSurface;

   /// Callbacks
   void (ESCALLBACK *drawFunc) ( void* );
   void (ESCALLBACK *keyFunc) ( void*, unsigned char, int, int );
   void (ESCALLBACK *updateFunc) ( void*, float deltaTime );
} ESContext;

        然後main中又聲明瞭一個UserData結構,這個其實是我們在當前指令碼中自行定義的結構體,用於存放一些使用者自行定義的資料,這裡我們可以看到結構體中只是封裝了一個GLunit資料,這個其實就是OpenGL中的無符號整型(正整型),相當於C語言中的unsigned int。

typedef struct
{
   // Handle to a program object
   GLuint programObject;
} UserData;

        這裡以es為字首的函式都是ES框架封裝的一些方法:

esInitContext:也是框架封裝好的一個方法,用於對最開始宣告的ESContext結構進行初始化;
esCreateWindow:此方法用於建立一個視窗並指定其寬度和高度,同時也設定了視窗左上角的標題。此處,是使用EGL在螢幕上建立一個渲染表面並附加到一個視窗上,EGL是跨平臺的,事OpenGL ES與本地視窗系統的中介,可以用它來建立渲染表面和上下文;

        Init:我們自行定義的初始化函式,主要用於初始化著色器、programObject等;

Draw:控制初始化後加載進來的頂點著色器片元著色器,繪製最後視窗真正要顯示出來的內容;

esRegisterDrawFunc:指定繪製回撥的方法為此指令碼中我們自行定義的Draw函式,如此係統每次繪製都會呼叫這個函式,渲染幀資料;

esMainLoop:進入主訊息迴圈,此處是無限迴圈,直到視窗關閉為止。

二、Init函式:
        在看完入口函式之後,我們接下來再看看入口函式中呼叫到的我們自定義的初始化函式Init:

///
// Initialize the shader and program object
//
int Init ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   GLbyte vShaderStr[] =  
      "attribute vec4 vPosition;    \n"
      "void main()                  \n"
      "{                            \n"
      "   gl_Position = vPosition;  \n"
      "}                            \n";
   
   GLbyte fShaderStr[] =  
      "precision mediump float;\n"\
      "void main()                                  \n"
      "{                                            \n"
      "  gl_FragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );\n"
      "}                                            \n";

   GLuint vertexShader;
   GLuint fragmentShader;
   GLuint programObject;
   GLint linked;

   // Load the vertex/fragment shaders
   vertexShader = LoadShader ( GL_VERTEX_SHADER, vShaderStr );
   fragmentShader = LoadShader ( GL_FRAGMENT_SHADER, fShaderStr );

   // Create the program object
   programObject = glCreateProgram ( );
   
   if ( programObject == 0 )
      return 0;

   glAttachShader ( programObject, vertexShader );
   glAttachShader ( programObject, fragmentShader );

   // Bind vPosition to attribute 0   
   glBindAttribLocation ( programObject, 0, "vPosition" );

   // Link the program
   glLinkProgram ( programObject );

   // Check the link status
   glGetProgramiv ( programObject, GL_LINK_STATUS, &linked );

   if ( !linked ) 
   {
      GLint infoLen = 0;

      glGetProgramiv ( programObject, GL_INFO_LOG_LENGTH, &infoLen );
      
      if ( infoLen > 1 )
      {
         char* infoLog = malloc (sizeof(char) * infoLen );

         glGetProgramInfoLog ( programObject, infoLen, NULL, infoLog );
         esLogMessage ( "Error linking program:\n%s\n", infoLog );            
         
         free ( infoLog );
      }

      glDeleteProgram ( programObject );
      return FALSE;
   }

   // Store the program object
   userData->programObject = programObject;

   glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );
   return TRUE;
}

        在這個函式中,首先我們定義了頂點著色器片元著色器原始碼,這是OpenGL ES 2.0進行繪製必不可少的兩個物件。

vShaderStr:頂點著色器原始碼——宣告一個vec4型別(包含四個float的向量)的輸入屬性vPosition;設定著色器的入口函式main函式並且在main函式中把vPosition賦值給內建輸出變數gl_Position,這個變數會傳給圖元裝配階段。

fShaderStr:片元著色器原始碼——為float型別變數指定一個預設的精度為mediump;在main函式中給內建輸出變數gl_FragColor複製為vec4(1.0,0.0,0.0,1.0),這裡就是用於指定片元輸出的顏色為紅色。

注:雖然在這裡我們將著色器原始碼以字串變數的形式放在程式碼中,但在實際開發中,我們通常將著色器原始碼放在檔案中,那樣更方便程式碼管理。)

        用LoadShader建立了頂點著色器和片元著色器之後,需要建立programObject物件(著色器物件跟programObject的關係就相當於編譯器和聯結器)。需要把著色器物件連線到programObject物件,一個programObject物件必須連線一個頂點著色器和一個片元著色器。

        glCreateProgram:建立一個programObject物件

        glAttachShader:著色器與programObject物件連線

glBindAttribLocation:設定頂點著色器屬性vPosition的位置

glGetProgramiv:獲取連線狀態

glClearColor:設定glClear時的背景顏色

連線正常後把programObject物件存放到UserData中。

三、LoadShader函式:

///
// Create a shader object, load the shader source, and
// compile the shader.
//
GLuint LoadShader ( GLenum type, const char *shaderSrc )
{
   GLuint shader;
   GLint compiled;
   
   // Create the shader object
   shader = glCreateShader ( type );

   if ( shader == 0 )
   	return 0;

   // Load the shader source
   glShaderSource ( shader, 1, &shaderSrc, NULL );
   
   // Compile the shader
   glCompileShader ( shader );

   // Check the compile status
   glGetShaderiv ( shader, GL_COMPILE_STATUS, &compiled );
   if ( !compiled ) 
   {
      GLint infoLen = 0;
      glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );
      if ( infoLen > 1 )
      {
         char* infoLog = malloc (sizeof(char) * infoLen );

         glGetShaderInfoLog ( shader, infoLen, NULL, infoLog );
         esLogMessage ( "Error compiling shader:\n%s\n", infoLog );            
         
         free ( infoLog );
      }
      glDeleteShader ( shader );
      return 0;
   }
   return shader;
}

這是著色器的載入函式,載入步驟:

1.glCreateShader:建立指定型別(頂點或片元)的著色器物件

2.glShaderSource:載入著色器的原始碼

3.glCompileShader:編譯著色器

4.glGetShaderiv:獲得著色器編譯狀態(用於判斷編譯是否成功,若成功則返回著色器物件)

四、Draw函式:

///
// Draw a triangle using the shader pair created in Init()
//
void Draw ( ESContext *esContext )
{
   UserData *userData = esContext->userData;
   GLfloat vVertices[] = {  0.0f,  0.5f, 0.0f, 
                           -0.5f, -0.5f, 0.0f,
                            0.5f, -0.5f, 0.0f };
      
   // Set the viewport
   glViewport ( 0, 0, esContext->width, esContext->height );
   
   // Clear the color buffer
   glClear ( GL_COLOR_BUFFER_BIT );

   // Use the program object
   glUseProgram ( userData->programObject );

   // Load the vertex data
   glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
   glEnableVertexAttribArray ( 0 );

   glDrawArrays ( GL_TRIANGLES, 0, 3 );

   eglSwapBuffers ( esContext->eglDisplay, esContext->eglSurface );
}
        Draw實現了繪製幀資料,具體步驟:

1.定義頂點資料座標

2.glViewport:通過OpenGL ES設定繪製的2D渲染表面的起點(x,y)、寬和高,也就是最終的繪製區域(可視區域)

3.glClear:以指定的顏色清除顏色緩衝區,也就是清屏,這裡使用的顏色就是Init初始化函式中glClearColor設定的顏色(故glClearColor需要在glClear之前執行)

4.glUseProgram:獲取渲染所要用到的program物件,這樣接下來的所有渲染都會使用連線到program物件的著色器

5.glVertexAttribPointer:載入幾何圖形和繪製圖元,此demo中我們指定的圖形是三角形,vVertices中定義了三角形的頂點座標組,然後把頂點位置載入到OpenGL ES,並連線頂點著色器定義的屬性vPosition。頂點著色器中的每個屬性都有一個唯一的位置(或者說是索引,是一個unsigned int值)。所以glVertexAttribPointer的作用就是把頂點資料載入到著色器中指定索引(例如索引我為0的屬性為vPosition)的屬性
6.glEnableVertexAttribArray:啟動通用頂點陣列屬性

7.glDrawArrays:告訴OpenGL ES要繪製的圖元是三角形

8.eglSwapBuffers:把快取中的資料顯示到繫結的螢幕上(雙緩衝技術:前臺緩衝區(螢幕可見)和後臺快取區(不可見),等到後臺緩衝區渲染完成後再與前臺緩衝區交換,如此便可實現平滑過渡)。

        eglSwapBuffers(esContext->eglDisplay, esContext->eglSurface);這是一個EGL函式,其輸入引數為EGL顯示區和視窗,這兩個引數代表了實際的物理顯示區和渲染區。

附加:

        關於EGL,EGL 是 Khronos 組織創造的渲染 API(OpenGL ES)和作業系統視窗之間的介面應用程式中引入EGL需要新增連結庫:libEGL.lib任何OpenGL ES在使用EGL渲染前需要做的事情:

1.查詢裝置上可用的顯示初始化它;

2.建立一個渲染螢幕(隱藏平面或顯示屏平面);

3.建立渲染上下文。