1. 程式人生 > 其它 >【專業技術】OPENGL與EGL

【專業技術】OPENGL與EGL

什麼是OpenGL ES

OpenGL ES是一套適用於手持嵌入式裝置的3DAPI。比如手機、PDA、汽車、航空等等上面都可以使用到OpenGL ES。OpenGL ES是免授權費的、跨平臺的、功能完善的2D和3D圖形應用程式介面API,它是桌面OpenGL的子集,是從OpenGL裁剪定製而來的。由於手持裝置的相關侷限性,OpenGL ES相對於OpenGL不可避免的進行了相關的精簡。去除了OpenGL中比如glBegin/glEnd,四邊形(GL_QUADS)、多邊形(GL_POLYGONS)等複雜圖元等許多非絕對必要的特性。但是OpenGL方面的很多知識,OpenGL ES都是可以借鑑的。 OpenGL ES其實是一個狀態機(State machine),它儲存一種狀態直至其改變。每個狀態都有本身預設的預設值,可以通過相關的查詢和設定函式進行相關的查詢和設定。 大多數的OpenGL ES的應用都採用的是相同的操作順序,這一系列的處理階段被稱作OpenGL ES的渲染管線(pipeline)。

什麼是EGL

OpenGL實現跨平臺的功能,在不同的作業系統上需要不同的類似適配層的內容,比如在Windows作業系統上需要WGL。同樣的,OpenGL ES是一個平臺中立的圖形庫,在它能夠工作前,需要與一個實際的視窗關聯起來,但是,與OpenGL不一樣的是,OpenGL是每個視窗系統需要一個與之對應的適配層,Windows需要WGL,X-Window需要xgl,Mac OS需要agl,而OpenGL ES的這層,是統一的一個標準,這個標準就是EGL。

下面大致介紹下EGL的使用,在surfaceflinger第1篇:surfaceflinger啟動中的初始化部分,曾經簡單描述了EGL的使用。結合程式碼來看基本和下面的使用步驟對應。

1. 獲取Display:

Display代表的是顯示器,有的系統上有多個顯示器,也就會有多個display。獲得Display需要呼叫EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id);,引數一般為EGL_DEFAULT_DISPLAY。該引數的實際意義是平臺相關的,比如在windows平臺上,一般返回的就是DC。

2. 初始化egl:

獲得了Display後,呼叫EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);該函式會進行一些相關的內部初始化工作。我們可以通過這個函式獲得egl的版本號。

3. 選擇Config:

Config實際就是FrameBuffer的引數,在Windows下對應於PixelFormat,在X-Window下對應Visual。可以用函式EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);,其中attrib_list 是以EGL_NONE結束的引數陣列,通常以id,value依次存放,對於個別標識性的屬性可以只有id,沒有value。另一個辦法是用EGLBoolean eglGetConfigs(EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config); 來獲得所有config。這兩個函式都會返回不多於config_size個Config,結果儲存在configs[]中,系統的總Config個數儲存在num_config中。可以利用eglGetConfig()中間兩個引數為0來查詢系統支援的Config總個數。Config有眾多的Attribute,這些Attribute決定FrameBuffer的格式和能力,通過eglGetConfigAttrib ()來讀取,但不能修改。

4. 構造Surface:

有了Config,就可以開始構造Surface了。Surface實際上就是一個FrameBuffer。通過函式EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config,EGLNativeWindowType win, const EGLint *attrib_list)可以建立一個Surface。系統通常還支援另外兩種Surface:PixmapSurface和PBufferSurface,這兩種都不是可顯示的Surface,PixmapSurface是儲存在系統記憶體中的點陣圖,PBuffer則是儲存在視訊記憶體中的幀。Surface也有一些attribute,基本上都可以故名思意,EGL_HEIGHT EGL_WIDTH EGL_LARGEST_PBUFFER EGL_TEXTURE_FORMAT EGL_TEXTURE_TARGET EGL_MIPMAP_TEXTURE EGL_MIPMAP_LEVEL,通過eglSurfaceAttrib()設定、eglQuerySurface()讀取。

5. 建立Context:

OpenGL ES的pipeline從程式的角度看就是一個狀態機,有當前的顏色、紋理座標、變換矩陣、渲染模式等一大堆狀態,這些狀態作用於程式提交的頂點座標等圖元從而形成幀緩衝內的畫素。在OpenGL ES的程式設計介面中,Context就代表這個狀態機,程式的主要工作就是向Context提供圖元、設定狀態,偶爾也從Context裡獲取一些資訊。用EGLContext eglCreateContext(EGLDisplay dpy, EGLSurface write, EGLSurface read, EGLContext * share_list)來建立一個Context。

6. 繪製:

應用程式通過OpenGL API進行繪製,一幀完成之後,呼叫eglSwapBuffers(EGLDisplay dpy, EGLContext ctx)來顯示。

這裡貼出一個OpenGL ES的hello world程式方便理解:

// 標頭檔案包含
#include "TG3.h"
#include
#include
#include "OGLES2Hello********.h"
  
// 巨集定義
#define VERTEX_ARRAY 0
  
// 定義Display、config、surface、context
EGLDisplay eglDisplay = 0;
EGLConfig eglConfig = 0;
EGLSurface eglSurface = 0;
EGLContext eglContext = 0;
EGLNativeWindowType eglWindow = 0;
  
// 沃Phone視窗指標
extern TWindow *g_pThis;
  
bool TestEGLError()
{
    //eglGetError返回上一個egl中的錯誤,使用者在每個egl函式呼叫結束都需要呼叫這個函式。
    EGLint iErr = eglGetError();
    if (iErr != EGL_SUCCESS)
    {
        return false;
    }
  
    return true;
}
  
bool CreateEGLContext()
{
    // 第一步:獲得或者建立一個可以用於OpenGL ES輸出的EGLNativeWindowType
    eglWindow = (EGLNativeWindowType)g_pThis;
  
    //第二步:獲得預設的Display。通常我們只有一塊螢幕,引數傳EGL_DEFAULT_DISPLAY就可以了。
    eglDisplay = eglGetDisplay((EGLNativeDisplayType) EGL_DEFAULT_DISPLAY);
  
    //第三步:初始化EGL,如果我們不想要版本號,後兩個引數也可以傳NULL進去。
    EGLint iMajorVersion, iMinorVersion;
    if (!eglInitialize(eglDisplay, &iMajorVersion, &iMinorVersion))
    {
        return false;
    }
  
    //第四步:指定需要的配置屬性,一個EGL的配置描述了Surfaces上畫素的格式資訊。當前我們要的是Windows的surface,在螢幕上是可見的,以EGL_NONE結尾。
    const EGLint pi32ConfigAttribs[] =
    {
        EGL_LEVEL, 0,
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_NATIVE_RENDERABLE, EGL_FALSE,
        EGL_DEPTH_SIZE, EGL_DONT_CARE,
        EGL_NONE
    };
  
    //第五步:尋找一個符合所有要求的配置,我們需要的只是其中一個,所以可以限制config的個數為1。
    int iConfigs;
    if (!eglChooseConfig(eglDisplay, pi32ConfigAttribs, &eglConfig, 1, &iConfigs) || (iConfigs != 1))
    {
        return false;
    }
  
    //第六步:建立一個可畫的surface。這裡建立時可見的windows surface。Pixmaps和pbuffers都是不可見的。
    eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, eglWindow, NULL);
  
    if(eglSurface == EGL_NO_SURFACE)
    {
        eglGetError(); // Clear error
        eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, NULL, NULL);
    }
  
    if (!TestEGLError())
    {
        return false;
    }
  
    //第七步:建立Context。我們OpenGL ES的資源,比如紋理只有在這個context裡是可見的。
    // 繫結API (可以是OpenGLES或者 OpenVG)
    eglBindAPI(EGL_OPENGL_ES_API);
    EGLint ai32ContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
    eglContext = eglCreateContext(eglDisplay, eglConfig, NULL, ai32ContextAttribs);
  
    if (!TestEGLError())
    {
        return false;
    }
  
    //第八步:將建立的context繫結到當前的執行緒,使用我們建立的surface進行讀和寫。Context繫結到執行緒上,你就可以不用擔心其他的程序影響你的OpenGL ES應用程式。
    eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
    if (!TestEGLError())
    {
        return false;
    }
    return true;
}
  
bool Render()
{
    //第九步:使用OpenGL ES API畫一些東西。到這裡,所有的東西都已準備就緒,我們做好了往螢幕上畫東西的準備。
    bool bRet = false;
  
    //單元矩陣,用於投影和模型變換
    float pfIdentity[] =
    {
        1.0f,0.0f,0.0f,0.0f,
        0.0f,1.0f,0.0f,0.0f,
        0.0f,0.0f,1.0f,0.0f,
        0.0f,0.0f,0.0f,1.0f
    };
  
    // Vertex和Fragment的shader
    // gl_FragColor指定了最終的畫素顏色。
    // gl_position則指定了最終的點在人眼座標中的位置。
    char szFragShaderSrc[] = {"
                              void main (void)
                              {
                              gl_FragColor = vec4(1.0, 1.0, 0.66 ,1.0);
                              }"};
    char szVertShaderSrc[] = {"
                              attribute highp vec4 myVertex;
                              uniform mediump mat4 myPMVMatrix;
                              void main(void)
                              {
                              gl_Position = myPMVMatrix * myVertex;
                              }"};
  
    char * pszFragShader = (char *)szFragShaderSrc;
    char * pszVertShader = (char *)szVertShaderSrc;
  
    GLuint uiFragShader = 0;
    GLuint uiVertShader = 0; /* 用來存放Vertex和Fragment shader的控制代碼 */ 
    GLuint uiProgramObject = 0; /* 用來存放建立的program的控制代碼*/ 
  
    GLint bShaderCompiled;
    GLint bLinked;
  
    // 我們要畫一個三角形,所以,我們先建立一個頂點緩衝區
    GLuint ui32Vbo = 0; // 頂點緩衝區物件控制代碼
  
    //頂點資料 這9個數據分別為3個點的X、Y、Z座標
    GLfloat afVertices[] = { -0.4f,-0.4f,0.0f, // Position
        0.4f ,-0.4f,0.0f,
        0.0f ,0.4f ,0.0f};
  
    int i32InfoLogLength, i32CharsWritten;
    char* pszInfoLog = NULL;
    int i32Location = 0;
    unsigned int uiSize = 0;
  
    //建立Fragment著色器物件
    uiFragShader = glCreateShader(GL_FRAGMENT_SHADER);
  
    // 將程式碼載入進來
    glShaderSource(uiFragShader, 1, (const char**)&pszFragShader, NULL);
  
    //編譯程式碼
    glCompileShader(uiFragShader);
  
    //看編譯是否成功進行
    glGetShaderiv(uiFragShader, GL_COMPILE_STATUS, &bShaderCompiled);
  
    if (!bShaderCompiled)
    {
        // 錯誤發生,首先獲取錯誤的長度
        glGetShaderiv(uiFragShader, GL_INFO_LOG_LENGTH, &i32InfoLogLength);
        //開闢足夠的空間來儲存錯誤資訊
        pszInfoLog = new char[i32InfoLogLength];
        glGetShaderInfoLog(uiFragShader, i32InfoLogLength, &i32CharsWritten, pszInfoLog);
        delete[] pszInfoLog;
        goto cleanup;
    }
  
    // 使用同樣的方法載入Vertex Shader
    uiVertShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(uiVertShader, 1, (const char**)&pszVertShader, NULL);
    glCompileShader(uiVertShader);
    glGetShaderiv(uiVertShader, GL_COMPILE_STATUS, &bShaderCompiled);
    if (!bShaderCompiled)
    {
        glGetShaderiv(uiVertShader, GL_INFO_LOG_LENGTH, &i32InfoLogLength);
        pszInfoLog = new char[i32InfoLogLength];
        glGetShaderInfoLog(uiVertShader, i32InfoLogLength, &i32CharsWritten, pszInfoLog);
        delete[] pszInfoLog;
        goto cleanup;
    }
  
    // 建立著色器程式
    uiProgramObject = glCreateProgram();
  
    // 將Vertex和Fragment Shader繫結進去。
    glAttachShader(uiProgramObject, uiFragShader);
    glAttachShader(uiProgramObject, uiVertShader);
  
    //將使用者自定義的頂點屬性myVertex繫結到VERTEX_ARRAY。
    glBindAttribLocation(uiProgramObject, VERTEX_ARRAY, "myVertex");
  
    // 連結著色器程式
    glLinkProgram(uiProgramObject);
  
    // 判斷連結是否成功的操作
    glGetProgramiv(uiProgramObject, GL_LINK_STATUS, &bLinked);
    if (!bLinked)
    {
        glGetProgramiv(uiProgramObject, GL_INFO_LOG_LENGTH, &i32InfoLogLength);
        pszInfoLog = new char[i32InfoLogLength];
        glGetProgramInfoLog(uiProgramObject, i32InfoLogLength, &i32CharsWritten, pszInfoLog);
        delete[] pszInfoLog;
        goto cleanup;
    }
  
    // 使用著色器程式
    glUseProgram(uiProgramObject);
  
    // 設定清除顏色,以RGBA的模式,每個分量的值從0.0到1.0
    glClearColor(0.6f, 0.8f, 1.0f, 1.0f);
  
    //生成一個頂點緩衝區物件
    glGenBuffers(1, &ui32Vbo);
  
    //繫結生成的緩衝區物件到GL_ARRAY_BUFFER
    glBindBuffer(GL_ARRAY_BUFFER, ui32Vbo);
  
    // 載入頂點資料
    uiSize = 3 * (sizeof(GLfloat) * 3); // Calc afVertices size (3 vertices * stride (3 GLfloats per vertex))
    glBufferData(GL_ARRAY_BUFFER, uiSize, afVertices, GL_STATIC_DRAW);
  
    // 畫三角形
    {
        //清除顏色緩衝區。glClear同樣也能清除深度緩衝區(GL_DEPTH_BUFFER)和模板緩衝區(GL_STENCIL_BUFFER_BIT)
        glClear(GL_COLOR_BUFFER_BIT);
  
  
        //獲取myPMVMatrix在shader中的位置
        i32Location = glGetUniformLocation(uiProgramObject, "myPMVMatrix");
  
        //傳值給獲取到的位置,也就是將pfIdentity傳給myPMVMatrix
        glUniformMatrix4fv( i32Location, 1, GL_FALSE, pfIdentity);
  
        //將VERTEX_ARRAY置為有效。
        glEnableVertexAttribArray(VERTEX_ARRAY);
  
        // 將定點資料傳到VERTEX_ARRAY
        glVertexAttribPointer(VERTEX_ARRAY, 3, GL_FLOAT, GL_FALSE, 0, 0);
  
        //畫一個三角形。
        glDrawArrays(GL_TRIANGLES, 0, 3);
  
        //SwapBuffers。就可以將三角形顯示出來
        eglSwapBuffers(eglDisplay, eglSurface);
    }
    bRet = true;
  
cleanup:
    // 釋放資源
    if (uiProgramObject)
        glDeleteProgram(uiProgramObject);
    if (uiFragShader)
        glDeleteShader(uiFragShader);
    if (uiVertShader)
        glDeleteShader(uiVertShader);
  
    // Delete the VBO as it is no longer needed
    if (ui32Vbo)
        glDeleteBuffers(1, &ui32Vbo);
  
    return bRet;
}
  
bool DestroyEGLContext()
{
    //第十步:結束OpenGL ES並刪除建立的Windows(如果存在的話).eglminate已經負責清除context和surface。所以呼叫了eglTerminate後就無需再呼叫eglDestroySurface和eglDestroyContext了。
    if (eglDisplay != EGL_NO_DISPLAY)
    {
        eglMakeCurrent(eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        eglDestroyContext(eglDisplay, eglContext );
        eglDestroySurface(eglDisplay, eglSurface );
        eglTerminate(eglDisplay);
        eglDisplay = EGL_NO_DISPLAY;
    }
  
    //第十一步:刪除已經建立的eglWindow。這個是跟具體平臺有關的。
    eglWindow = NULL;
    return true;
}
  
//這樣,有關OpenGL ES畫一個三角形的步驟就走完了,剩下的,只是在沃Phone的平臺上進行相關額外的操作,比如視窗的建立,訊息迴圈的處理等。
//根據工程嚮導新建一個新工程,工程新建完,在**MainForm.cpp中
//新增定義:
TWindow *g_pThis = NULL;
  
//在TMainForm::TMainForm(TApplication * pApp):TWindow(pApp)中加入
    m_bGLESInit = FALSE;
    g_pThis = this;
  
//在case EVENT_WinInit:中加入:
            if (CreateEGLContext())
            {
                m_bGLESInit = TRUE;
                Sys_PostMessage(MESSAGE_PRIOR_LOWEST, 0, &PID_SELF, EVENT_FirstUser, GetWindowHwndId(), 0, NULL, 0);
            }
            bHandled = TRUE;
  
//新增:
    case EVENT_FirstUser:
        {
            if(m_bGLESInit)
            {
                Render();
                Sys_PostMessage(MESSAGE_PRIOR_LOWEST, 0, &PID_SELF, EVENT_FirstUser, GetWindowHwndId(), 0, NULL, 0);
            }
            bHandled = TRUE;
        }
        break;
    case EVENT_GlesUpdateNotify:
        {
            if(m_bGLESInit)
            {
                TRectangle rt;
                GetClientBounds(&rt);
                MarkUpdateRectangle(&rt);
            }
            bHandled = TRUE;
        }
        break;
  
//在case EVENT_WinClose:中加入:
            if (m_bGLESInit)
            {
                DestroyEGLContext();
                m_bGLESInit = FALSE;
            }
            // Stop the application since the main form has been closed
            pApp->SendStopEvent();

OpenGL ES和EGL在Android中

SurfaceFlinger是android系統GUI的核心,但相對於OpenGL ES來講,它其實只是一個“應用”。在android的GUI系統中,有EGL/OpenGLES,又有SurfaceFlinger、GraphicPlane、DisplayHardware、Gralloc、FramebufferNativeWindow等一系列模組,下面的圖可以很好的說明各個模組之間的關係:

根據上面的圖,可以有以下分析及結論:

  1. Linux核心提供了統一的framebuffer顯示驅動,裝置節點/dev/graphics/fb*或者/dev/fb*,以fb0表示第一個Monitor,當前實現中只用到了一個顯示屏;
  2. Android的HAL層提供了Gralloc,分為fb和gralloc兩個裝置。前者負責開啟核心中的framebuffer、初始化配置,以及提供post、setSwapInterval等操作,後者則管理幀緩衝區的分配和釋放。上層只能通過Gralloc訪問幀緩衝區,這樣一來就實現了有序的封裝保護;
  3. 由於OpenGL ES是一個通用的函式庫,在不同的平臺系統上需要被“本地化”——即把它與具體平臺上的視窗系統建立起關聯,這樣才能保證它正常工作。從FramebufferNativeWindow這個名稱就能判斷出來,它就是將OpenGL ES在Android平臺上本地化的中介之一。後面我們還會看到應用程式端所使用的另一個“本地視窗”。為OpengGL ES配置本地視窗的是EGL;
  4. OpenGL或者OpenGL ES 更多的只是一個介面協議,實現上既可以採用軟體,也能依託於硬體。這一方面給產品開發帶來了靈活性,我們可以根據成本與市場定位來決定具體的硬體設計,從而達到很好的定製需求;另一方面,既然有多種實現的可能,那麼OpenGL ES在執行時是如何取捨的呢?這也是EGL的作用之一。它會去讀取egl.cfg這個配置檔案,然後根據使用者的設定來動態載入libagl(軟體實現)或者libhgl(硬體實現),然後上層才可以正常使用各種glXXX介面;
  5. SurfaceFlinger中持有一個GraphicPlane成員變數mGraphicPlanes來描述“顯示屏”;GraphicPlane類中又包含了一個DisplayHardware物件例項(mHw)。具體是在SurfaceFlinger::readyToRun中,完成對它們的建立與初始化。並且DisplayHardware在初始化時還將呼叫eglInitialize、eglCreateWindowSurface等介面,利用EGL來完成對OpenGLES環境的搭建。 其中: surface =eglCreateWindowSurface(display, config, mNativeWindow.get(), NULL); mNativeWindow 就是一個FramebufferNativeWindow物件。DisplayHardware為OpenGL ES設定了“本地化”所需的視窗;
  6. 很多模組都可以呼叫OpenGLES提供的API(這些介面以“gl”為字首,比如glViewport、glClear、glMatrixMode、glLoadIdentity等等),包括SurfaceFlinger、DisplayHardware等;
  7. 與OpenGL ES相關的模組,可以分為如下幾類: 配置類:即幫助OpenGL ES完成配置的,包括EGL、DisplayHardware都可以認為是這一類; 依賴類:也就是OpenGL ES要正常執行起來所依賴的“本地化”的東西,上圖中是指FramebufferNativeWindow; 使用類:使用者也可能是配置者,比如DisplayHardware既扮演了“幫助”OpenGL的角色,同時它也是其使用方。另外只要處在與OpenGL ES同一個環境(Context)中的模組,都可以使用它來完成操作,比如SurfaceFlinger如果是對EGL的作用、工作方式以及它所提供的重要介面等有不明白的,強烈建議大家先閱讀官方文件以及本書應用篇中的章節,否則會大大影響後面的學習和理解。

參考文件:

http://www.bianceng.cn/OS/extra/201306/36755.htm

http://blog.csdn.net/mkhgg/article/details/6738912(原帖沒有找到)