解讀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視窗表面(螢幕)。
從工程的結構樹我們可以看到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.建立渲染上下文。