OpenGL ES 2.0 着色器和着色程序API

本文记录着色器shader和着色程序shader program的概览及使用,具体的API用法参考官网:
https://www.khronos.org/opengles/sdk/docs/man/

着色器和着色程序

当使用着色器来渲染时,需要创建两种基础对象:着色器对象shader objects和着色程序对象program objects。可以参考C语言中:编译器对源代码进行编译得到目标代码,然后由链接器来链接目标代码,形成最终的程序。 OpenGL ES 2.0中采用与此类似的机制:着色器对象是包含有一个着色器的对象,需要为其添加着色器源代码,然后编译成目标格式。编译之后,着色器对象就可以添加给着色程序对象,在OpenGL ES中每个程序对象会包含两个着色器对象,一个顶点着色器和一个片段着色器。着色程序对象被链接成为一个最终的“可执行文件”,这个最终的程序才可以用来渲染。
整个工作流程如下:首先创建一个顶点着色器对象和一个片段着色器对象,然后分别对其添加源代码,并且编译;然后创建一个着色程序对象,将编译后的着色器对象添加给它,并完成链接。如果一切正常的话,就可以通过OpenGL来调用该着色程序了。

着色器相关API

创建一个shader对象

1
GLuint glCreateShader(GLenum type)

删除shader对象

1
void glDeleteShader(GLuint shader)

向已创建的shader对象提供Shader源码

1
void glShaderSource(GLuint shader, GLsizei count, const char** string, const GLint* length)

编译shader内部源码

1
void glCompileShader(GLuint shader)

获得shader对象的状态信息,如是否编译成功等

1
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params)

获得shader对象的log记录

1
void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei *length, GLchar *infoLog)

接下来是一段示例代码,加载一个着色器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
}

着色程序相关API

创建着色程序对象

1
GLuint glCreateProgram(void)

删除着色程序对象

1
void glDeleteProgram(GLuint program)

给着色程序添加着色器对象

1
void glAttachShader(GLuint program, GLuint shader)

删除着色程序中的着色器对象

1
void glDetachShader(GLuint program, GLuint shader)

链接着色程序

1
void glLinkProgram(GLuint program)

获得着色程序对象的状态信息,如是否链接成功等

1
void glGetProgramiv(GLuint program, GLenum pname, GLint *params)

获得着色程序对象的log记录

1
void glGetProgramInfoLog(GLuint program, GLsizei maxLength, GLsizei *length, GLchar *infoLog)

验证着色程序对象

1
void glValidateProgram(GLuint program)

激活着色程序对象

1
void glUseProgram(GLuint program)

下边是一段示例代码,创建着色程序,为着色程序添加着色器,并链接着色程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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 FALSE;
}
// ...
// Use the program object
glUseProgram(userData->programObject);

Uniform和Attribute

完成着色程序的链接之后,即可以获取着色程序中的uniform数据,uniform存储的是由应用程序通过OpenGL API传入shader的只读常量。同一个着色程序共享同一组uniform,如果某个着色程序的顶点着色器和片段着色器如果同时声明了相同的uniform,此uniform在两个shader中必须保持同样的类型和值。

Uniform相关API

为获取着色程序中激活的uniform(当一个uniform被着色程序使用时,认为该uniform处于激活状态。如果在shader中声明了一个uniform,但并没有使用,则链接时会把它优化掉,所以获取激活uniform参数列表时不会包含它),首先需要调用glGetProgramiv并使用GL_ACTIVE_UNIFORMS作为参数,将会返回着色程序中激活状态的uniform的数量。同时还可以调用 glGetProgramiv 使用GL_ACTIVE_UNIFORM_MAX_LENGTH为参数获取名称最长的uniform变量名中包含的字符个数(含字符串终止符)。然后,使用glGetActiveUniform来获取各uniform的详细信息,具体如下:

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

使用glGetActiveUniform可以获取uniform的几乎全部参数,包括变量名、变量类型等,还可以查询某变量是否是数组,及数组中元素个数等。当知道了uniform的名称之后,即可获取它的位置值(location,一个整数),给uniform设置数值时必须使用它的位置值。

1
GLint glGetUniformLocation(GLuint program, const char* name)

这个函数即是通过uniform的名称来获取位置值,如果在着色程序中没有对应于该名称的激活的uniform值,则会返回-1。通过位置值给uniform加载变量值的函数如下,依据uniform的变量类型不同而不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void glUniform1f(GLint location, GLfloat x)
void glUniform1fv(GLint location, GLsizei count,
const GLfloat* v)
void glUniform1i(GLint location, GLint x)
void glUniform1iv(GLint location, GLsizei count, const GLint* v)
void glUniform2f(GLint location, GLfloat x, GLfloat y) void glUniform2fv(GLint location, GLsizei count, const GLfloat* v)
void glUniform2i(GLint location, GLint x, GLint y)
void glUniform2iv(GLint location, GLsizei count, const GLint* v)
void glUniform3f(GLint location, GLfloat x, GLfloat y, GLfloat z)
void glUniform3fv(GLint location, GLsizei count, const GLfloat* v)
void glUniform3i(GLint location, GLint x, GLint y, GLint z)
void glUniform3iv(GLint location, GLsizei count, const GLint* v)
void glUniform4f(GLint location, GLfloat x, GLfloat y, GLfloat z, GLfloat w);
void glUniform4fv(GLint location, GLsizei count, const GLfloat* v)
void glUniform4i(GLint location, GLint x, GLint y, GLint z, GLint w)
void glUniform4iv(GLint location, GLsizei count, const GLint* v)
void glUniformMatrix2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value)
void glUniformMatrix3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value)
void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value)

在使用这些函数时,直接根据需要的变量类型选择即可。使用glGetActiveUniform可以查询uniform的类型。注意,glUniform\*系列函数,并不需要着色程序对象的句柄值作为参数,因为glUniform\*函数总是作用于当前使用的程序对象(即调用glUseProgram使用的着色程序对象)。uniform会被着色程序对象保留,相当于对应着色程序的局部变量,一旦在一个着色程序中设置了uniform,当激活另一个着色程序时,之前的uniform值会依然保留。
示例代码, 如何使用函数来获取着色程序中的uniform信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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; index++)
{
GLint size; GLenum type; GLint location;
// Get the Uniform Info
glGetActiveUniform(progObj, index, maxUniformLen, NULL, &size, &type, uniformName);
// Get the uniform location
location = glGetUniformLocation(progObj, uniformName);
switch(type)
{
case GL_FLOAT:
// ... break;
case GL_FLOAT_VEC2: // ...
break;
case GL_FLOAT_VEC3: // ...
break;
case GL_FLOAT_VEC4: // ...
break;
case GL_INT:
// ...
break;
// ... Check for all the types ...
default:
// Unknown type
break;
}
}

Attribute相关API

除了从着色程序中获取uniform之外,还需要对着色器程序中的vertex attributes进行设置。获取attributes的方法与获取uniform的方法类似。首先通过GL_ACTIVE_ATTRIBUTES可以得到已激活attributes的列表,然后通过glGetActiveAttrib获取某一个attribute的参数,最后通过一系列的设置函数来给attribute加载需要的值。

着色器编译器和着色器二进制文件

着色器编译器产生的代码会占用存储空间,并且着色器的编译过程也会消耗很多的CPU时间和内存,这对于便携式或手持设备的性能影响还是相当明显的。考虑到这些因素,OpenGL ES 2.0还有一种实现形式可以支持二进制形式着色器,即预先离线完成着色器源码的编译工作(offline),生成二进制形式的着色器。这种预编译工具通常是由各厂商制作,因此并没有统一标准,虽然丧失了一些通用性与便携性,但是的确可以减轻运行时OpenGL ES的负担。可以通过接口glGetBooleanv检查GL_SHADER_COMPILER来确定所使用的实现形式是否支持在线编译(online)。
对于支持在线编译(online)的情况,用glShaderSource定义着色器,当完成着色器的编译之后,可以使用glReleaseShaderCompiler来告知系统编译完毕,占用的资源已经允许释放。但是如果后续还会使用glCompileShader来编译多个着色器,则需要重新为其分配资源,如同glReleaseShaderCompiler没有被调用过一样。

1
void glReleaseShaderCompiler(void)

对于只支持二进制形式着色器的OpenGL ES实现,则厂商需要来定义其自身支持的二进制格式,下边的代码用来判断是否支持着色器编译器、是否支持二进制着色器以及支持哪一种二进制类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
GLboolean shaderCompiler; 
GLint numBinaryFormats;
GLint *formats;

// Determine if a shader compiler available
glGetBooleanv(GL_SHADER_COMPILER, &shaderCompiler);
// Determine binary formats available
glGetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &numBinaryFormats);
formats = malloc(sizeof(GLint) * numBinaryFormats);

glGetIntegerv(GL_SHADER_BINARY_FORMATS, formats);

// "formats" now holds the list of supported binary formats

如果使用二进制形式的着色器,并且有一份供应商提供的二进制着色器的token,则可以使用glShaderBinary来加载该着色器。

1
void glShaderBinary(GLint n, const GLuint* shaders, GLenum binaryFormat, const void* binary, GLint length)

与在线编译相似,在线链接功能(online linking)也需要消耗很多的CPU时间及存储空间。有的厂商要求在二进制着色器里同时包含顶点着色器和片断着色器,有的厂商则提供单独的顶点二进制着色器和片段二进制着色器,然后在线将其链接起来。

直接提供的二进制着色器,与着色器源码编译后得到的目标文件具有相同的功能,唯一的不同在于使用着色器源码的目标文件的话,可以通过一定的方法读回着色器的源代码,但直接提供的二进制文件不能。在应用开发过程中,可以根据实现方式的支持程度来选择使用源代码着色器还是二进制的着色器,

REFERENCE

https://www.khronos.org/opengles/sdk/docs/man/xhtml/
OpenGL ES 2.0 ProgrammingGuide