1. 程式人生 > >【OpenGL ES】著色器Shader與程式Program

【OpenGL ES】著色器Shader與程式Program

在OpenGL ES 3程式中,Shader和Program是兩個重要的概念,至少需要建立一個頂點Shader物件、一個片段Shader物件和一個Program物件,才能用著色器進行渲染,理解Shader物件和Program物件的最佳方式是將它們比作C語言的編譯器和連結程式,從Shader的建立到Program的連結共六個基本步驟,建立Shader、載入Shader原始碼、編譯Shader、建立Program、繫結Program與Shader、連結Program,下面介紹Shader和Program的建立方法及相關概念。

1、建立Shader

GLuint glCreateShader(GLenum shaderType)
;

建立著色器使用glCreateShader,成功時返回一個空的著色器物件控制代碼,一個正整數,失敗時返回0,shaderType有誤時產生錯誤GL_INVALID_ENUM,shaderType可以是:

GL_VERTEX_SHADER // vertex
GL_FRAGMENT_SHADER // fragment
GL_COMPUTE_SHADER // from GL ES 3.1
GL_TESS_CONTROL_SHADER // from GL ES 3.2
GL_TESS_EVALUATION_SHADER // from GL ES 3.2
GL_GEOMETRY_SHADER // from GL ES 3.2

與glCreateShader對應的有個glDeleteShader,用於刪除著色器物件,包括記憶體釋放和shader控制代碼釋放,如果這個著色器物件attach到了一個程式物件,那麼這個著色器物件將作個刪除標記而不會立即刪除,等到不再attach任何程式物件時再刪除,引數shader無效時產生錯誤GL_INVALID_VALUE。

void glDeleteShader(GLuint shader);

對一個著色器物件來說,判斷其是否有刪除標記,可以呼叫如下函式:

glGetShaderiv(shader, GL_DELETE_STATUS, params);

2、載入Shader原始碼

void glShaderSource(GLuint shader,
    GLsizei count,
    const GLchar **string,
    const GLint *length);

建立了著色器物件之後,通過glShaderSource載入著色器原始碼,替換原有的著色器原始碼,shader為著色器物件控制代碼,count為string陣列和length陣列的大小,string為指標陣列,儲存了count個著色器原始碼,length為整型陣列,指定了每個著色器原始碼的長度,如果length為NULL,則認為著色器原始碼字串以null結尾,length為正數時只加載指定長度的著色器原始碼字串,如果lengh不合適,可能導致錯誤產生,length為負數時也認為著色器原始碼字串以null結尾。載入失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION。glShaderSource拷貝了對應的著色器原始碼,所以之後就可以立即釋放相關的記憶體了。

3、編譯Shader

void glCompileShader(GLuint shader);

成功載入著色器原始碼之後,通過glCompileShader編譯著色器物件,處理上一步載入的著色器原始碼字串,shader為前面建立的著色器物件控制代碼,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION。

當OpenGL ES編譯和繫結著色器時,著色器程式碼通常解析為某種中間表現形式,這和大部分編譯語言相同,如抽象語法樹,編譯器必須將抽象形式轉換為硬體的機器指令,理想狀態下,這個編譯器還應該進行大量的優化,如無用程式碼刪除、常量傳播等,進行這些工作需要付出代價,主要是CPU時間和記憶體。OpenGL ES 3.0實現必須支援線上著色器編譯,用glGetBooleanv檢索的GL_SHADER_COMPILER值必須是GL_TRUE,可以指定著色器使用glShaderSource,還可以嘗試緩解著色器編譯對資源的影響,也就是說,一旦完成了應用程式中著色器的編譯,就可以呼叫如下glReleaseShaderCompiler,這個函式提示OpenGL ES實現已經完成了著色器編譯的工作,可以釋放它的資源了。需要注意的是,這個函式只是一個提示,如果決定用glCompileShader編譯更多的著色器,那麼OpenGL ES實現需要重新為編譯器分配資源。

void glReleaseShaderCompiler(void);

4、檢查Shader狀態

void glGetShaderiv(GLuint shader,
    GLenum pname,
    GLint *params);
void glGetShaderInfoLog(GLuint shader,
    GLsizei maxLength,
    GLsizei *length,
    GLchar *infoLog);

glGetShaderiv用於檢查Shader狀態,shader為著色器物件控制代碼,pname為待檢查的Shader狀態,params返回檢查的Shader狀態,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION、GL_INVALID_ENUM,Shader狀態即函式引數pname取值如下:

GL_SHADER_TYPE:著色器型別,如GL_VERTEX_SHADER等。
GL_DELETE_STATUS:著色器是否有刪除標記。
GL_COMPILE_STATUS:著色器是否編譯成功。
GL_INFO_LOG_LENGTH:著色器通知日誌長度,包括null結尾的字元。
GL_SHADER_SOURCE_LENGTH:著色器原始碼字串長度,包括null結尾的字元。

glGetShaderInfoLog用於獲取Shader通知日誌的字串表示,shader為著色器物件控制代碼,maxLength為獲取通知日誌的buffer的最大長度,length返回獲取的通知日誌的長度,不包括null結尾的字元,可以為NULL,infoLog返回通知日誌,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION。

另外,還有幾個有用的函式,glIsShader判斷是否為著色器物件,glGetShaderSource獲取著色器原始碼的字串表示。

GLboolean glIsShader(GLuint shader);
void glGetShaderSource(GLuint shader,
    GLsizei bufSize,
    GLsizei *length,
    GLchar *source);

5、建立Program

GLuint glCreateProgram(void);

建立程式物件使用glCreateProgram,glCreateProgram建立一個空的程式物件,一個非零值,失敗時返回0。與glCreateProgram對應的有個glDeleteProgram,用於刪除程式物件,包括記憶體釋放和program控制代碼釋放,如果這個程式物件在使用中,那麼將作個刪除標記而不會立即刪除,等到不再使用時再刪除,其中的著色器物件將自動detach,著色器物件有刪除標記就刪除,否則不刪除。引數program無效時產生錯誤GL_INVALID_VALUE。

void glDeleteProgram(GLuint program);

對一個程式物件來說,判斷其是否有刪除標記,可以呼叫如下函式:

void glGetProgramiv(program, GL_DELETE_STATUS, params);

6、繫結Program與Shader

void glAttachShader(GLuint program,
    GLuint shader);

建立了程式物件之後,通過glAttachShader將其繫結到一個著色器物件,program和shader為對應的程式物件和著色器物件,同一種類型的著色器物件只能繫結一次,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION。與glAttachShader對應的有個glDetachShader,用於解除繫結,program和shader為對應的程式物件和著色器物件,如果著色器物件有刪除標記且沒有被其它地方使用將刪除,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION。

void glDetachShader(GLuint program,
    GLuint shader);

7、連結Program

void glLinkProgram(GLuint program);

程式物件與著色器物件attach之後,使用glLinkProgram連結程式物件,這是使用程式物件前的最後一步,引數program為需要連結的程式物件控制代碼,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION。程式物件連結成功之後,所有活動的使用者定義的uniform變數都被初始化為0而且被設定一個位置,這個位置使用glGetUniformLocation獲取,所有活動的命名uniform塊中的uniform變數都被設定一個偏移量,包括陣列和矩陣,所有活動的使用者定義的attribute變數沒有繫結到一般的頂點屬性索引時將在此時繫結一個。連結操作負責生成最終的可執行程式,連結階段是生成在硬體上執行的最終指令的時候,連結程式將檢查各種物件的數量,確保成功連結,例如,連結程式將確保頂點著色器寫入片段著色器使用的所有頂點著色器輸出變數並用相同的型別宣告,確保任何在頂點和片段著色器中都宣告的統一變數和統一變數緩衝區的型別相符,確保最終的程式符合具體實現的限制,如屬性、統一變數、輸入輸出著色器變數的數量等。連結程式物件可能會失敗,常見原因可參照https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glLinkProgram.xhtml

8、檢查Program狀態

void glGetProgramiv(GLuint program,
    GLenum pname,
    GLint *params);
void glGetShaderInfoLog(GLuint shader,
    GLsizei maxLength,
    GLsizei *length,
    GLchar *infoLog);

檢查Program狀態的方法類似於上面介紹的檢查Shader狀態的方法,只是使用的函式不同,分別為glGetProgramiv和glGetShaderInfoLog,glGetProgramiv的引數pname支援的狀態包括(詳細用法可參照https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glGetProgramiv.xhtml):

GL_ACTIVE_ATOMIC_COUNTER_BUFFERS
GL_ACTIVE_ATTRIBUTES
GL_ACTIVE_ATTRIBUTE_MAX_LENGTH
GL_ACTIVE_UNIFORM_BLOCKS
GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH
GL_ACTIVE_UNIFORMS
GL_ACTIVE_UNIFORM_MAX_LENGTH
GL_ATTACHED_SHADERS
GL_COMPUTE_WORK_GROUP_SIZE
GL_DELETE_STATUS
GL_GEOMETRY_LINKED_INPUT_TYPE
GL_GEOMETRY_LINKED_OUTPUT_TYPE
GL_GEOMETRY_LINKED_VERTICES_OUT
GL_GEOMETRY_SHADER_INVOCATIONS
GL_INFO_LOG_LENGTH
GL_LINK_STATUS
GL_PROGRAM_BINARY_RETRIEVABLE_HINT
GL_PROGRAM_SEPARABLE
GL_TESS_CONTROL_OUTPUT_VERTICES
GL_TESS_GEN_MODE
GL_TESS_GEN_POINT_MODE
GL_TESS_GEN_SPACING
GL_TESS_GEN_VERTEX_ORDER
GL_TRANSFORM_FEEDBACK_BUFFER_MODE
GL_TRANSFORM_FEEDBACK_VARYINGS
GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH
GL_VALIDATE_STATUS

下面是一個建立Shader和Program的程式碼例子:

// esShader.c
GLuint esLoadShader ( 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;
}

GLuint ESUTIL_API esLoadProgram ( const char *vertShaderSrc, const char *fragShaderSrc )
{
   GLuint vertexShader;
   GLuint fragmentShader;
   GLuint programObject;
   GLint linked;

   // Load the vertex/fragment shaders
   vertexShader = esLoadShader ( GL_VERTEX_SHADER, vertShaderSrc );
   if ( vertexShader == 0 )
   {
      return 0;
   }
   fragmentShader = esLoadShader ( GL_FRAGMENT_SHADER, fragShaderSrc );
   if ( fragmentShader == 0 )
   {
      glDeleteShader ( vertexShader );
      return 0;
   }

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

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

   // 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 0;
   }

   // Free up no longer needed shader resources
   glDeleteShader ( vertexShader );
   glDeleteShader ( fragmentShader );

   return programObject;
}

9、使用著色器程式

void glUseProgram(GLuint program);

最後,成功連結程式物件之後,就可以使用這個著色器程式了,後面在新增一些頂點資料就可以進行圖元繪製了。glUseProgram把這個program程式物件安裝到當前渲染狀態的一部分,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION。

另外,還有幾個有用的函式,glIsProgram判斷是否為程式物件,glGetIntegerv(GL_CURRENT_PAROGRAM, data)獲取當前活動的程式物件,glGetAttachedShaders獲取attach到一個程式物件上的著色器物件,glValidateProgram用於驗證程式。

GLboolean glIsProgram(  GLuint program);
void glGetIntegerv(GLenum pname, GLint * data); // glGetXxx
void glGetAttachedShaders(GLuint program,GLsizei maxCount,GLsizei *count,GLuint *shaders);
void glValidateProgram(GLuint program);

10、uniform變數查詢與設定

程式物件連結成功之後,就可以在物件上進行許多查詢,如活動的uniform變數,是著色語言中的只讀變數,uniform變數分為兩種型別,預設uniform變數和uniform變數塊,如下所示:

// default
uniform mat4 matViewProj;
uniform mat3 matNormal;
uniform mat3 matTexGen;

// block
uniform TransformBlock
{
    mat4 matViewProj;
    mat3 matNormal;
    mat3 matTexGen;
};

如果uniform變數在頂點著色器和片段著色器中均有宣告,則宣告的型別必須相同,且在兩個著色器中的值也需相同。查詢活動uniform變數之前,可以使用GL_ACTIVE_UNIFORMS引數呼叫glGetProgramiv函式,獲得活動uniform變數的個數,包括前面提到的兩種型別的uniform變數和內建uniform變數,還可以使用GL_ACTIVE_UNIFORM_MAX_LENGTH獲得uniform變數名的最大長度,然後就可以使用如下幾個函式查詢活動uniform變量了。

void glGetActiveUniform(GLuint program,
    GLuint index,
    GLsizei bufSize,
    GLsizei *length,
    GLint *size,
    GLenum *type,
    GLchar *name);

glGetActiveUniform獲取程式物件program中位置index的活動uniform變數的資訊,bufSize指定name的最大長度,length返回name的實際長度,可以為NULL,uniform變數非陣列時size為1,uniform變數是陣列時size為使用的最大陣列元素,type返回uniform變數的型別,name返回uniform變數名,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION。type返回的uniform變數型別與著色語言中的變數型別有個對應關係,如GL_FLOAT對應於float,詳細對應關係可參照https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glGetActiveUniform.xhtml

void glGetActiveUniformsiv(GLuint program,
    GLsizei uniformCount,
    const GLuint *uniformIndices,
    GLenum pname,
    GLint *params);

glGetActiveUniformsiv獲取程式物件program中uniformCount個uniform活動變數的型別為pname的資訊,uniformIndices是個長度為uniformCount的儲存了uniform變數位置的陣列,params返回對應的結果,也是個長度為uniformCount的陣列,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION、GL_INVALID_ENUM。pname型別如下:

GL_UNIFORM_TYPE
GL_UNIFORM_SIZE
GL_UNIFORM_NAME_LENGTH
GL_UNIFORM_BLOCK_INDEX
GL_UNIFORM_OFFSET
GL_UNIFORM_ARRAY_STRIDE
GL_UNIFORM_MATRIX_STRIDE
GL_UNIFORM_IS_ROW_MAJOR

知道了活動uniform變數名之後,通過glGetUniformLocation可以獲取其位置,失敗時返回-1,可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION。

GLint glGetUniformLocation(GLuint program,
    const GLchar *name);

然後,通過uniform變數的位置就可以使用glGetUniformXxx查詢其值或者使用glUniformXxx改變其值了,失敗時可能的錯誤為GL_INVALID_VALUE、GL_INVALID_OPERATION,下面是這兩個函式的某一個版本。

void glGetUniformfv(GLuint program,
    GLint location,
    GLfloat *params);
void glUniform1f(GLint location,
    GLfloat v0);

下面是示例程式碼:

GLint maxUniformLen;
GLint numUniforms;
char *uniformName;
GLint index;

glGetProgramiv(progObj, GL_ACTIVE_UNIFORMS, &numUniforms);
glGetProgramiv(progObj, GL_ACTIVE_UNIFORM_MAX_LENGTH, &maxUniformLen);
uniformName = malloc(sizeof(char) * maxUniformLen);

for (index = 0; index < numUniforms; ++i) {
    Glint len;
    GLint size;
    GLenum type;
    GLint location;

        // get the uniform info
    glGetAcitveUniform(progObj, index, maxUniformLen, &len, &size, &type, uniformName);
        // get the uniform location
    location = glGetUniformLocation(progObj, uniformName);
}

11、uniform變數緩衝區物件

可以使用緩衝區物件儲存uniform變數資料,從而在程式中的著色器之間甚至程式之間共享uniform變數,這種緩衝區物件稱作uniform變數緩衝區物件,使用uniform變數緩衝區物件,可以在更新大的uniform變數塊時降低API開銷,而且增加了uniform變數的可用儲存,可以不受預設uniform變數塊大小的限制,更新uniform變數緩衝區物件時可以使用glBufferData建立和初始化一個緩衝區物件、glBufferSubData更新緩衝區物件、glMapBufferRange對映緩衝區物件的一個區域、glUnmapBuffer使用緩衝區物件前解除對映的資料等。

與統一變數位置值用於引用統一變數類似,統一變數塊索引用於引用統一變數塊,可以用glGetUniformBlockIndex獲取命名統一變數塊的索引。從統一變數塊索引,可以用glGetActiveBlockName和glGetActiveUniformBlockiv確定活動統一變數塊的細節,獲取統一變數塊的許多屬性,命名統一變數塊的索引必須小於GL_ACTIVE_UNIFORM_BLOCKS的值。

GLuint glGetUniformBlockIndex(GLuint program,
    const GLchar *uniformBlockName);
void glGetActiveUniformBlockName(GLuint program,
    GLuint uniformBlockIndex,
    GLsizei bufSize,
    GLsizei *length,
    GLchar *uniformBlockName);
void glGetActiveUniformBlockiv(GLuint program,
    GLuint uniformBlockIndex,
    GLenum pname,
    GLint *params);

glGetActiveUniformBlockiv的pname可以是:

GL_UNIFORM_BLOCK_BINDING
GL_UNIFORM_BLOCK_DATA_SIZE
GL_UNIFORM_BLOCK_NAME_LENGTH
GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS
GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES
GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER
GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER

一旦有了統一變數塊索引,就可以呼叫glUniformBlockBinding,將該索引和程式中的統一變數塊繫結點關聯,索引不能超過GL_MAX_UNIFORM_BUFFER_BINDINGS的值。最後,可以用glBindBufferRange或者glBindBufferBase將統一變數緩衝區物件繫結到GL_UNIFORM_BUFFER目標和程式中的統一變數塊繫結點。

void glUniformBlockBinding(GLuint program,
    GLuint uniformBlockIndex,
    GLuint uniformBlockBinding);
void glBindBufferRange(GLenum target,
    GLuint index,
    GLuint buffer,
    GLintptr offset,
    GLsizeipt rsize);
void glBindBufferBase(GLenum target,
    GLuint index,
    GLuint buffer);

上面函式的target如下,需要注意的是不同型別的target其offset是不同的,可以用glGetXxx查詢其限制。

GL_ATOMIC_COUNTER_BUFFER // from GL ES 3.1 (4)
GL_SHADER_STORAGE_BUFFER // from GL ES 3.1 (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT)
GL_TRANSFORM_FEEDBACK_BUFFER // (4)
GL_UNIFORM_BUFFER // (GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT)

下面是示例程式碼:

GLuint blockId, bufferId;
GLint blockSize;
GLuint bindingPoint = 1;
GLfloat lightData[] =
{
    // lightDirection (padded to vec4 based on std140 rule)
    1.0f, 0.0f, 0.0f, 0.0f,
    // lightPosition
    0.0f, 0.0f, 0.0f, 1.0f
}

// retrive the uniform block index
blockId = glGetUniformBlockIndex(program, "LightBlock");
// associate the uniform block index with a binding point
glUniformBlockBinding(program, blockId, bindingPoint);
// get the size of lightData
// alternatively we can calculate it using sizeof(lightData) in this example
glGetAcitveUniformBlockiv(program, blockId, GL_UNIFORM_BLOCK_DATA_SIZE, &blockSize);
// create and fill a buffer object
glGenBuffers(1, &bufferId);
glBindBuffer(GL_UNIFORM_BUFFER, bufferId);
glBufferData(GL_UNIFORM_BUFFER, blockSize, lightData, GL_DYNAMIC_DRAW);
// bind the buffer object to the uniform block bindint point
glBindBufferBase(GL_UNIFORM_BUFFER, bindingPoint, buffer);

12、屬性查詢

上面介紹了uniform變數的查詢和設定方法,我們還需要使用程式物件設定頂點屬性,對頂點屬性的查詢和統一變數查詢非常相似,可以用GL_ACTIVE_ATTRIBUTES查詢找到活動屬性列表,GL_ACTIVE_ATTRIBUTE_MAX_LENGTH查詢活動屬性名字的最大長度,可以用如下glGetActiveAttrib等函式找到某個屬性的特性、設定頂點陣列以載入頂點屬性值等。glGetXxx(GL_MAX_VERTEX_ATTRIBS)還可以查詢最大的頂點屬性。

void glGetActiveAttrib(GLuint program,
    GLuint index,
    GLsizei bufSize,
    GLsizei *length,
    GLint *size,
    GLenum *type,
    GLchar *name);

13、著色程式二進位制碼

著色程式二進位制碼是完全編譯和連結的著色程式的二進位制表示,可以儲存到檔案供以後使用,避免了線上編譯的代價,也不用在實現中分發著色器原始碼,涉及如下兩個函式:

void glGetProgramBinary(GLuint program,
    GLsizei bufsize,
    GLsizei *length,
    GLenum *binaryFormat,
    void *binary);
void glProgramBinary(GLuint program,
    GLenum binaryFormat,
    const void *binary,
    GLsizei length);

glGetProgramBinary用於獲取程式二進位制碼,program為程式物件控制代碼,bufsize為可以寫入binary的最大位元組數,不能小於GL_PROGRAM_BINARY_LENGTH(用glGetProgramiv獲取),否則產生錯誤GL_INVALID_OPERATION,length為二進位制資料的位元組數,可以為NULL,binaryFormat為供應商專用二進位制格式標誌,binary為著色器編譯器生成的二進位制資料指標。glProgramBinary用於將程式二進碼讀回OpenGL ES實現,program為程式物件控制代碼,binaryFormat為供應商專用二進位制格式標誌,binary為著色器編譯器生成的二進位制資料指標,length為二進位制資料的位元組數,失敗時可能的錯誤為GL_INVALID_OPERATION、GL_INVALID_ENUM,相關函式涉及glGetXxx的引數GL_NUM_PROGRAM_BINARY_FORMATS和GL_PROGRAM_BINARY_FORMATS。