OpenGL ES繪製一個圖形
為了介紹OpenGL ES 2.0的基本概念,我們從一個簡單的例子開始。在本章中,我們展示了建立一個繪製單個三角形的OpenGL ES 2.0程式需要什麼。我們將要編寫的程式只是一個繪製幾何圖形的OpenGL ES 2.0應用程式的最基本的例子。本章涵蓋了許多概念:
- 使用EGL建立螢幕渲染表面。(這可以在GLSurfaceView中看到相關操作)
- 載入頂點和片段著色器 (open gl es部分)
- 建立程式物件,附加頂點和片段著色器,以及連結程式物件
- 設定視口
- 清除顏色緩衝區
- 渲染一個簡單的圖元
- 使顏色緩衝區的內容在EGL視窗表面可見。(EGL部分)
總結:使用一個案例來說明opengles的建立過程,以及羅列出建立的具體步驟。
事實證明,在我們開始用OpenGL ES 2.0繪製三角形之前,需要相當多的步驟。本章將介紹這些步驟的基礎知識。在本書的後面,我們將詳細介紹這些步驟,並進一步記錄應用程式設計介面。我們在這裡的目的是讓你執行你的第一個簡單的例子,這樣你就可以知道用OpenGL ES 2.0建立一個應用程式需要什麼。
補充:使用android的GLSurfaceView就不需要出現這裡的EGL部分,它已經幫助我們實現了,在書本中的案例裡面會有EGL的建立過程,然後最後將快取區中的畫素刷新出去。
簡單說一下EGL,它是opengl和螢幕之間的一箇中間層。open gl 是跨平臺的,所以需要一箇中間層(我個人是這麼認為的),下一章會有一章來說明和EGL的操作步驟。
你好三角形
讓我們看看我們的Hello Triangle示例程式的完整原始碼,它在示例2-1中列出。對於那些熟悉固定函式桌面OpenGL的讀者來說,你可能會認為這只是為了畫一個簡單的三角形而編寫的大量程式碼。對於那些不熟悉桌面OpenGL的人來說,你可能也會認為這是一大堆程式碼,只是為了畫一個三角形!請記住,OpenGL ES 2.0是完全基於著色器的,這意味著如果沒有載入和繫結適當的著色器,您就不能繪製任何幾何圖形。這意味著渲染所需的設定程式碼比使用固定函式處理的桌面OpenGL多。
Example 2-1 Hello Triangle Example #include "esUtil.h" typedef struct { // Handle to a program object GLuint programObject; } UserData; /// // Create a shader object, load the shader source, and // compile the shader. // GLuint LoadShader(const char *shaderSrc, GLenum type) { 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; } /// // 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, 1.0f); return TRUE; } /// // 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); } int main(int argc, char *argv[]) { ESContext esContext; UserData userData; esInitialize(&esContext); esContext.userData = &userData; esCreateWindow(&esContext, "Hello Triangle", 320, 240, ES_WINDOW_RGB); if(!Init(&esContext)) return 0; esRegisterDrawFunc(&esContext, Draw); esMainLoop(&esContext); } Building and Running the Examples 25
這個程式碼繪製一個三角形。
使用Opengles框架
主函式做的第一件事是宣告一個ESContext並初始化它,
ESContext esContext;
UserData userData;
esInitialize(&esContext);
esContext.userData = &userData
ESContext被傳遞到所有的ES框架實用程式函式中,幷包含了ES框架所需的關於程式的所有必要資訊。傳遞上下文的原因是示例程式和ES程式碼框架不需要使用任何全域性資料。
主函式的其餘部分負責建立視窗、初始化draw回撥函式以及進入主迴圈:
esCreateWindow(&esContext, "Hello Triangle", 320, 240,
ES_WINDOW_RGB);//建立視窗的大小
if(!Init(&esContext))
return 0;
esRegisterDrawFunc(&esContext, Draw);
esMainLoop(&esContext);
openGLES部分
建立頂點著色器和片段著色器
必須創建出正確的片元和片段著色器才可以繪製出圖形。第一個任務就是創建出並載入。
GLbyte vShaderStr[] =
"attribute vec4 vPosition; \n"
"void main() \n"
"{ \n"
" gl_Position = vPosition; \n"
"}; \n";
很簡單的幾行,輸入一個屬性,這個就是後面將要傳遞進來的頂點座標,將頂點座標給內建變數gl_Position。
複習一下:
- 頂點著色器的輸入:屬性、uniform、sample等,這個使用到了屬性。
片段著色器
GLbyte fShaderStr[] =
"precision mediump float; \n"
"void main() \n"
"{ \n"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); \n"
"}
這個也比較簡單,第一句,設定精度是float(第一章說過這個是在opengles上才有的,opengl並沒有)。
gl_FragColor是一個內建變數,這個值就是顯示到螢幕的值(這麼說也不準確,需要經過一系列的測試之後才顯示,可能會帶有其他操作,比如拋棄、混合等)
編譯和載入著色器
下一步就是載入編譯著色器,對其進行錯誤檢查無誤之後就可以附著到程式上了。
讓我們看看LoadShader函式是如何工作的。著色器物件首先使用glCreateShader建立,它建立指定型別的新著色器物件。
GLuint LoadShader(GLenum type, const char *shaderSrc)
{
GLuint shader;
GLint compiled;
// Create the shader object
shader = glCreateShader(type);//傳入type,片段和片元
if(shader == 0)
return 0;
}
著色器原始碼本身使用glShaderSource載入到著色器物件中。然後使用glCompileShader函式編譯著色器。
// 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;
}
建立程式物件並連結著色器
應用程式為頂點和片段著色器建立了著色器物件,它就需要建立一個程式物件。從概念上講,程式物件可以被認為是最終的連結程式。一旦每個著色器被編譯成一個著色器物件,它們必須在繪製前附加到一個程式物件並連結在一起。
// Create the program object
programObject = glCreateProgram();
if(programObject == 0)
return 0;
glAttachShader(programObject, vertexShader);
glAttachShader(programObject, fragmentShader);
連線成功之後就可以獲取屬性位置,給屬性設定值了。
這裡也說另一個方法,設定頂點著色器的位置
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;
一切準備就緒,就可以使用程式了 。
// Use the program object
glUseProgram(userData->programObject);
設定視口並清除顏色緩衝區
現在我們已經用EGL建立了一個渲染表面,並初始化和載入了著色器,我們準備好實際繪製一些東西了。繪圖回撥函式繪製框架。我們在Draw中執行的第一個命令是glViewport,它通知OpenGL ES將要繪製的2D渲染表面的原點、寬度和高度。在OpenGL ES中,視口定義了2D矩形,所有OpenGL ES渲染操作最終都將在該矩形中顯示。
glViewport(0, 0, esContext->width, esContext->height);
這個在起始位置0,0,就是這螢幕在左上角,後面兩個引數是顯示在寬高。這個書本上在案例是顯示出建立處理螢幕的尺寸。
設定好螢幕之後,下一步就是清除螢幕。螢幕清除的有顏色快取區, 深度快取區等。使用掩碼的方式寫入。
// Clear the color buffer
glClear(GL_COLOR_BUFFER_BIT);
載入幾何圖形的頂點
GLfloat vVertices[] = {0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f};
…
// Load the vertex data
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vVertices);
glEnableVertexAttribArray(0);
glDrawArrays(GL_TRIANGLES, 0, 3);
創建出頂點,然後獲取頂點的位置,並且設定使能,最後進行一次繪製。頂點就是告訴圖元,我們繪製的是一個什麼圖形,這個圖形是什麼樣的,使用哪些頂點。
回顧一下:
頂點之後的操作,圖元裝配,裁剪,剔除。裁剪主要裁剪的是否在視錐體內部,剔除是根據設定的頂點的正方向還是反方向繪製。然後進行柵格化,柵格化就是將圖元變為一個二維陣列的形式,並且也會計算差值。
顯示後臺緩衝區
螢幕上可見的幀緩衝區由畫素資料的二維陣列表示。我們可以考慮在螢幕上顯示影象的一種可能的方法是在我們繪製時簡單地更新可見幀緩衝區中的畫素資料。然而,直接更新可顯示緩衝區上的畫素有一個嚴重的問題。也就是說,在典型的顯示系統中,物理螢幕從幀緩衝儲存器以固定的速率更新。如果一種是直接繪製到框架緩衝區中,使用者可以看到工件作為顯示的框架緩衝區的部分更新,為了解決這個問題,使用了一種稱為雙緩衝的系統。在這個方案中,有兩個緩衝區:前緩衝區和後緩衝區。所有渲染都發生在後臺緩衝區,該緩衝區位於螢幕不可見的記憶體區域。所有渲染完成後,該緩衝區將與前緩衝區(或可見緩衝區)進行“交換”。然後,前緩衝區成為下一幀的後緩衝區。
使用這種技術,我們不顯示一個可見的表面,直到一個幀的所有渲染完成。在OpenGL ES應用程式中,這一切都是通過EGL控制的。這是通過一個名為eglSwapBuffers的EGL函式實現的:
eglSwapBuffers(esContext->eglDisplay, esContext->eglSurface);
交換緩衝區之後,我們終於在螢幕上看到了我們的三角形!
總結:這章相對比較簡單,通過一個完整的案例,來繪製一個三角形,這個繪製包含了所有的繪製所必須得步驟。
- 準備頂點
- 準備片元和片段著色器
- 載入編譯
- 建立程式
- 附著程式
- 使用程式
- 獲取輸入引數的位置
- 設定值
- 繪製
上面都是繪製open gl 相關的,還需要繪製螢幕的表面,使用EGL得到螢幕資訊,建立繪製表面。 最後將兩個快取區交換。