天天看點

OpenGL的Context(Profile)

OpenGL在渲染的時候需要一個Context,這個Context記錄了OpenGL渲染需要的所有資訊,可以把它了解成一個大的結構體,它裡面記錄了目前繪制使用的顔色、是否有光照計算以及開啟的光源等非常多我們使用OpenGL函數調用設定的狀态和狀态屬性。在OpenGL 3.0版本之前,OpenGL建立Context都是一緻的,随着更新會新增一些内容(例如從OpenGL1.1更新到1.5,會新增一些狀态變量或者屬性,并添加一些設定這些内容的函數),整體上來說沒有什麼大的變化。但是從OpenGL 3.0開始,OpenGL為了擺脫曆史的“包袱”,想要徹底的廢棄掉之前的許多特性,但是無奈市面上已經有大量依賴OpenGL之前版本的代碼,導緻OpenGL維護小組的這一想法難以付諸實施,于是在OpenGL 3.1開始引入了OpenGL Context的一些分類,比如引入了CoreProfile等概念,之後随着版本發展到3.3,一切算是确定下來。

正是由于OpenGL的這一變化過程以及其間引入的各種概念,導緻初學者學習OpenGL時非常的痛苦,本來OpenGL API對于一般開發者來說就是晦澀難懂,如今還引入這些内容真的是令開發者壓力山大。反正我在看OpenGL Spec的時候查閱了很多資料,對這些内容還是一頭霧水。本文主要論述一下這些内容,有可能我了解的并不完全正确,有錯誤的地方希望讀者在評論中指正。

1. Context的種類

在OpenGL 3.0版本之前,所有的OpenGL Context是統一的,都是一種相容之前版本的模式(例如使用OpenGL 1.1編寫的代碼,在支援OpenGL 2.1的裝置上可以正常的運作)。這些版本的OpenGL API也被稱之為固定管線(相比較之後引入shader的可程式設計管線來說), 但是從3.0開始有了變化:

  • 1、 OpenGL 3.0 引入了廢棄機制,标記了許多OpenGL的函數是廢棄的(但是3.0并沒有真正移除它們,也就是說3.0版本仍然是一個可以向後相容的Context)
  • 2、 OpenGL 3.1釋出删除了之前3.0标記的過時函數(固定管線相關的函數),但是為了之前的OpenGL代碼可用, 引入了一個擴充ARB_compatibility,這個擴充可以讓OpenGL 3.1支援之前的OpenGL固定管線的内容。

但是ARB_compatibility有許多問題:它将OpenGL的核心函數core profile和之前的固定管線函數綁定在了一個合集内,也就是說實作core profile函數集不能包含compatibility中不存在的函數,使用者不能夠要求一種Context隻包含core profile的内容,這個擴充定義的函數是一個合集,core profile隻是它的一個子集。兩個關系類似于:(也就是說類似于一個套餐,你不能隻想要你需要的那部分)

OpenGL的Context(Profile)
  • 3、 正是由于這個問題,在OpenGL 3.2中正式引入了模式的概念(Profile),模式就将二者分離開來了。OpenGL 3.2 中模式包括:

(1) Core Profile 隻包含最新的Shader相關的函數,程式必須使用Shader編寫

(2)compatibility Profile 可以相容之前的OpenGL固定管線的内容,也可以使用Core Profile中的内容

這樣分開之後,開發者可以顯式的要求建立某一種Profile來使用

  • 4、 3.2以後的版本直到OpenGL目前最新的版本(OpenGL 4.6)都是按照這種模式來做的。

另外還有一個模式的稱之為:Forward compatibility

這個是什麼意思呢?第一眼看好像是向前相容的感覺(有點類似于compatibility的意思),實際上并不是,它的真正含義是向“未來”相容,什麼是面向“未來”呢?說白了就是把所有OpenGL API中标記有deprecated标簽的函數都禁用。(隻對OpenGL 3.0及其以上版本有作用),具體來說是:

(1) OpenGL 3.0(3.0中标記了一些函數是deprecated),如果設定該标記,那麼這些函數都不可用;

(2) OpenGL 3.1 (所有3.0和3.1中标記為deprecated的函數都不可用);

(3) OpenGL 3.2+ compatibility Profile Context: 沒有任何作用,畢竟人家是compatibility的(向後相容);

(4) OpenGL 3.2+ core Profile Context: 凡是标記為deprecated的函數都不可用;

OpenGL中有幾個特殊的版本号,在查詢API的時候非常有用:

1. OpenGL 2.1 (是所有固定管線API的合集,不包含任何coreprofile的新增的API,也稱之為Legecy OpenGL)

2. OpenGL 3.3 (第一個core profile的版本,雖然3.2就有,但是一般以3.3版本作為core profile代表)

3. OpenGL最新版本(目前是OpenGL 4.6),定義了最新的特性

一般查詢這幾個版本的API就可以了,有一個查詢OpenGL API非常實用的網站:http://docs.gl/ ,比OpenGL官網的API手冊更加實用。裡面提供了 gl2、gl3、gl4 的查詢,對應的就是上文所說的 OpenGL 2.1、 OpenGL 3.3、 OpenGL最新版本。

2. OpenGL Core Profile Only

在某些情況下,特别是學習現代OpenGL程式設計的過程中,我們需要OpenGL完全廢棄掉之前的固定管線的調用,也就是說我們調用了固定管線的内容,OpenGL可以給我們一個編譯或者是連結運作錯誤,讓我們了解到使用了Legecy的内容,這樣友善我們集中精力到CoreProfile的内容上,知道自己調錯了函數。

問題是:怎麼做到這一點呢?

2.1 FreeGLUT

首先是FreeGlut中提供的解決方案:在GLUT中并沒有相關的内容,畢竟GLUT出現在OpenGL 1.1版本,之後也鮮有維護更新,于是繼承它的FreeGLUT開始引入Context選擇的内容,在FreeGLUT中提供了下面三個函數:

glutInitContextVersion( int majorVersion, int minorVersion );
    glutInitContextFlags( int flags );
    glutInitContextProfile( int profile );
           

在閱讀API手冊之後,我檢視了自己的顯示卡,提示已經支援到OpenGL 4.5,于是在代碼中這樣調用

glutInitContextVersion(, );
    glutInitContextProfile(GLUT_CORE_PROFILE);
    glutInitContextFlags(GLUT_FORWARD_COMPATIBLE);
           

以期望獲得OpenGL CoreProfile 4.5的特性,但是不要引入LegecyOpenGL。但是結果是”然并卵“,并不會影響我調用glVertex這樣的函數,還是可以正常的渲染glVertex這樣的調用。

2.2 GLFW

GLFW是一個相對較新的OpenGL圖形界面架構,目前最新的版本是3.2.1。作用類似于GLUT,但是功能更加強大,支援OpenGL ES和Vulkan等特性,GLFW的更詳細介紹可以參考網站 glfw

在GLFW中也提供了函數用來設定OpenGL的Contex,函數如下:

void glfwWindowHint(int hint, int value);
           

如果想建立一個隻有CoreProfile的Context,可以設定:

glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, );
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, );
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
           

上面的調用建立了一個OpenGL 3.3 版本的Profile,經過測試,發現GLFW提供了函數是有效果的,在使用CoreProfile建立的Context調用OpenGL Legecy中的函數并不起作用。如果需要使用Legecy OpenGL,可以用相容模式(Compatibility Profile)的方式建立OpenGL Context,需要修改為:

glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, );
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, );
    //glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
           

使用下面的測試程式可以看到,當使用Coreprofile時,場景什麼也沒有,改用相容模式後,場景可以正确渲染出三角形。

#pragma comment(lib, "glfw3dll.lib")
#pragma comment(lib, "glew32.lib")
#pragma comment(lib, "opengl32.lib")

#include <GL/glew.h>
#include <GLFW/glfw3.h>


#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = ;
const unsigned int SCR_HEIGHT = ;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, );
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, );
    //glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);

    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -;
    }

    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // ---------------------------------------
    GLenum err = glewInit();
    if (GLEW_OK != err) {
        fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
        return ;
    }

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        glLoadIdentity();
        glTranslated(, , -);
        glBegin(GL_TRIANGLES);
        glVertex2f(, );
        glVertex2f(, );
        glVertex2f(, );
        glEnd();


        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return ;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(, , width, height);
}
           

也就是說GLFW是可以做到這一點的,推薦新手在學習OpenGL Core Profile時使用這個架構。

3. OpenGL擴充引入方式

OpenGL隻是一個标準/規範,具體的實作是由驅動開發商針對特定顯示卡實作的。由于OpenGL驅動版本衆多,它大多數函數的位置都無法在編譯時确定下來,需要在運作時查詢。是以任務就落在了開發者身上,開發者需要在運作時擷取函數位址并将其儲存在一個函數指針中供以後使用。取得位址的方法因平台而異,在Windows上會是類似這樣:

// 定義函數原型
typedef void (*GL_GENBUFFERS) (GLsizei, GLuint*);
// 找到正确的函數并指派給函數指針
GL_GENBUFFERS glGenBuffers  = (GL_GENBUFFERS)wglGetProcAddress("glGenBuffers");
// 現在函數可以被正常調用了
GLuint buffer;
glGenBuffers(, &buffer);
           

你可以看到代碼非常複雜,而且很繁瑣,我們需要對每個可能使用的函數都要重複這個過程。幸運的是,有些庫能簡化此過程,下文提到的這兩個庫就提供了這種功能

3.1 GLEW

GLEW(The OpenGL Extension Wrangler Library)是一個跨平台C/C++庫,用來提供導出OpenGL函數的功能,使用方式非常簡單,在OpenGL渲染的Context(一般是視窗)建立好後,調用glewInit即可,代碼如下:

GLenum err = glewInit();
if (GLEW_OK != err)
{
  /* Problem: glewInit failed, something is seriously wrong. */
  fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
  ...
}
fprintf(stdout, "Status: Using GLEW %s\n", glewGetString(GLEW_VERSION));
           

之後就可以直接調用OpenGL的擴充函數了,glew除此之外還提供了查詢某種OpenGL擴充在顯示卡中是否支援,查詢的方式是使用某種擴充的枚舉變量,例如,查詢是否支援頂點shader,代碼如下:

if (GLEW_ARB_vertex_program)
{
  /* It is safe to use the ARB_vertex_program extension here. */
  glGenProgramsARB(...);
}
           

上文中使用GLFW的示例使用了GLEW,GLEW有一個缺陷是它并沒有提供一種方式可以屏蔽OpenGL Legecy函數的調用,上文中測試的代碼盡管我們使用Core profile的方式,但是代碼中仍然存在 glVetex、glBegin這樣固定管線OpenGL的函數調用(雖然它們在Core Profile模式下沒有任何作用),看起來不那麼統一。要做到這一點可以使用下面的庫GLAD

3.2 GLAD

GLAD并不是一個OpenGL的庫,它是一個網絡服務,提供了根據我們選擇的OpenGL要求生成OpenGL的導出庫的源碼,可以通路它的網站:GLAD Service,進入之後想根據裡面提供的選項,像選菜單一樣選擇需要的OpenGL 版本和Profile,之後點選生成便可生成對應版本OpenGL,GLAD應該提供給你了一個zip壓縮檔案,包含兩個頭檔案目錄,和一個glad.c檔案。将兩個頭檔案目錄(glad和KHR)複制到你的Include檔案夾中(或者增加一個額外的項目指向這些目錄),并添加glad.c檔案到你的工程中。

OpenGL的Context(Profile)

使用方法也很簡單,在産生OpenGL Context之後,調用下面的初始化代碼:

//glfw中的使用方式
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -;
    }
           

之後就可以正常調用OpenGL擴充函數了。

這樣做讓glad.h中僅僅隻包含我們想要擴充的頭檔案,例如在設定3.3 + CoreProfile版本之後,可以嚴格控制頭檔案中隻有這些内容,是以凡是代碼中有LegecyOpenGL的調用都會在編譯的時候給出錯誤提示。

3.3 其他

除了上面兩個庫之外,還有其他的一些三方庫,包括Glee、glext等類似的庫,提供的都是類似的功能,在此不再贅述。

參考資料:

1. Forcing OpenGL Core Profile Only

2.Can I explicitly disable deprecated OpenGL functions in my code?

3. Forcing Core Profile in GLEW?

4. Creating a window

5. OpenGL Context

6. Learning OpenGL CN 建立視窗

7. The OpenGL Extension Wrangler Library

繼續閱讀