着色器的最佳实践
着色器提供了很大的灵活性,但如果执行太多的计算或执行效率低下,它们也可能是一个重大瓶颈.
初始化期间编译和链接着色器
与其他OpenGL ES状态更改相比,创建着色器程序是一项昂贵的操作。在应用程序初始化时编译,链接和验证程序。一旦您创建了所有着色器,应用程序可以通过调用glUseProgram来有效地切换它们。
在调试时检查着色器程序错误
在编译或链接着色器程序之后阅读诊断信息在您的应用的版本构建中不是必需的,并且可以降低性能。使用OpenGL ES函数仅在应用程序的开发构建中读取着色器编译或链接日志,如清单10-1所示
Listing 10-1
/ After calling glCompileShader, glLinkProgram, or similar
#ifdef DEBUG
// Check the status of the compile/link
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLen);
if(logLen > 0) {
// Show any errors as appropriate
glGetProgramInfoLog(prog, logLen, &logLen, log);
fprintf(stderr, “Prog Info Log: %s\n”, log);
}
#endif
同样,您只能在开发版本中调用glValidateProgram函数。您可以使用此功能来查找开发错误,例如无法绑定着色器程序所需的所有纹理单元。但是,由于验证程序会对整个OpenGL ES上下文状态进行检查,所以这是一项昂贵的操作。由于程序验证的结果仅在开发过程中有意义,因此您不应该在应用程序的版本版本中调用此函数
使用单独的着色器对象来加速编译和链接
许多OpenGL ES应用程序使用几个顶点和片段着色器,通常有用的是使用不同的顶点着色器重复使用相同的片段着色器,反之亦然。因为核心的OpenGL ES规范要求在单个着色器程序中将顶点和片段着色器链接在一起,混合和匹配着色器会导致大量程序,从而在初始化应用程序时增加总着色器编译和链接时间。
iOS上的OpenGL ES 2.0和3.0上下文支持EXT_separate_shader_objects扩展名。您可以使用此扩展提供的函数分别编译顶点和片段着色器,并使用程序管道对象在渲染时间混合和匹配预编译着色器阶段。此外,该扩展提供了一个简化的界面,用于编译和使用着色器,如清单10-2所示
Listing10-2
- (void)loadShaders
{
const GLchar *vertexSourceText = " ... vertex shader GLSL source code ... ";
const GLchar *fragmentSourceText = " ... fragment shader GLSL source code ... ";
// Compile and link the separate vertex shader program, then read its uniform variable locations
_vertexProgram = glCreateShaderProgramvEXT(GL_VERTEX_SHADER, 1, &vertexSourceText);
_uniformModelViewProjectionMatrix = glGetUniformLocation(_vertexProgram, "modelViewProjectionMatrix");
_uniformNormalMatrix = glGetUniformLocation(_vertexProgram, "normalMatrix");
// Compile and link the separate fragment shader program (which uses no uniform variables)
_fragmentProgram = glCreateShaderProgramvEXT(GL_FRAGMENT_SHADER, 1, &fragmentSourceText);
// Construct a program pipeline object and configure it to use the shaders
glGenProgramPipelinesEXT(1, &_ppo);
glBindProgramPipelineEXT(_ppo);
glUseProgramStagesEXT(_ppo, GL_VERTEX_SHADER_BIT_EXT, _vertexProgram);
glUseProgramStagesEXT(_ppo, GL_FRAGMENT_SHADER_BIT_EXT, _fragmentProgram);
}
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
// Clear the framebuffer
glClearColor(0.65f, 0.65f, 0.65f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Use the previously constructed program pipeline and set uniform contents in shader programs
glBindProgramPipelineEXT(_ppo);
glProgramUniformMatrix4fvEXT(_vertexProgram, _uniformModelViewProjectionMatrix, 1, 0, _modelViewProjectionMatrix.m);
glProgramUniformMatrix3fvEXT(_vertexProgram, _uniformNormalMatrix, 1, 0, _normalMatrix.m);
// Bind a VAO and render its contents
glBindVertexArrayOES(_vertexArray);
glDrawElements(GL_TRIANGLE_STRIP, _indexCount, GL_UNSIGNED_SHORT, 0);
}
尊重着色器的硬件限制
OpenGL ES限制您可以在顶点或片段着色器中使用的每个变量类型的数量。当超出这些限制时,OpenGL ES规范不需要实现提供软件回退;相反,着色器根本无法编译或链接。开发应用程序时,必须确保在着色器编译期间不发生错误,如清单10-1所示
使用精确提示
精确提示被添加到GLSL ES语言规范中,以满足与嵌入式设备的较小硬件限制匹配的紧凑型着色器变量的需要。每个着色器必须指定默认精度;单个着色器变量可以覆盖此精度,以向编译器提供如何在应用程序中使用该变量的提示。使用OpenGL ES实现不需要使用提示信息,但可以这样做来生成更有效的着色器。 GLSL ES规范列出了每个提示的范围和精度
重要提示:精确提示所定义的范围限制不会被强制执行。您不能假定您的数据被夹紧到此范围
遵循以下准则:
- 当有疑问时,默认为高精度。
- 通常可以使用低精度变量来表示0.0到1.0范围内的颜色。
- 位置数据通常应以高精度存储。
- 照明计算中使用的常数和矢量通常可以作为中等精度存储。
- 降低精度后,重新测试您的应用程序,以确保结果符合您的期望
清单10-3默认为高精度变量,但使用低精度变量计算颜色输出,因为不需要更高的精度
Listing 10-3
precision highp float; // Defines precision for float and float-derived (vector/matrix) types.
uniform lowp sampler2D sampler; // Texture2D() result is lowp.
varying lowp vec4 color;
varying vec2 texCoord; // Uses default highp precision.
void main()
{
gl_FragColor = color * texture2D(sampler, texCoord);
}
着色器变量的实际精度可以在不同的iOS设备之间变化,以及每个精度级别的操作性能。有关设备特定的注意事项,请参阅iOS设备兼容性参考
执行矢量计算懒惰
不是所有的图形处理器都包括矢量处理器;他们可以在标量处理器上执行向量计算。在着色器中执行计算时,请考虑操作顺序,以确保即使在标量处理器上执行计算也能高效执行。
如果清单10-4中的代码在向量处理器上执行,则每个乘法将在所有四个向量的组件中并行执行。然而,由于括号的位置,标量处理器上的相同操作将进行八次乘法,即使三个参数中的两个是标量值
Listing10-4
highp float f0, f1;
highp vec4 v0, v1;
v0 = (v1 * f0) * f1;
通过移动括号,可以更有效地执行相同的计算,如清单10-5所示。在这个例子中,首先将标量值相乘,结果与矢量参数相乘;整个操作可以用五次乘法计算
Listing10-5
highp float f0, f1;
highp vec4 v0, v1;
v0 = v1 * (f0 * f1);
类似地,如果您的应用程序不使用结果的所有组件,则应始终为向量操作指定写掩码。在标量处理器上,可以跳过未在掩码中指定的组件的计算。清单10-6在标量处理器上运行两倍,因为它指定只需要两个组件
Listing10-6
highp vec4 v0;
highp vec4 v1;
highp vec4 v2;
v2.xz = v0 * v1;
使用常量,而不是在着色器中计算值
无论何时可以在着色器之外计算值,将其作为均匀或常数传递到着色器。在着色器中重新计算动态值可能会非常昂贵。
使用分支说明小心
分支机构在着色器中不鼓励,因为它们可以降低在3D图形处理器上并行执行操作的能力(尽管在OpenGL ES 3.0功能的设备上降低了性能成本)。
如果你完全避免分支,你的应用程序可能会表现最好。例如,不是使用许多条件选项创建大型着色器,而是为特定的渲染任务创建更小的着色器。在减少着色器中的分支数量和增加您创建的着色器数量之间存在折衷。测试不同的选项并选择最快的解决方案。
如果您的着色器必须使用分支机构,请遵循以下建议:
- 最佳表现:在着色器编译时常用的常数分支。
- 可接受:在统一变量上分支。
- 潜在的慢:在着色器上计算的值上分支
消除循环
您可以通过展开循环或使用向量来执行操作来消除许多循环。例如,这段代码效率很低:
int i;
float f;
vec4 v;
for(i = 0; i < 4; i++)
v[i] += f;
可以直接使用组件方式添加相同的操作:
float f;
vec4 v;
v += f;
当您不能消除循环时,优选循环具有恒定的限制以避免动态分支
避免在着色器中计算阵列指数
使用在着色器中计算的索引比常数或统一的数组索引更昂贵。访问统一数组通常比访问临时数组便宜
意识到动态纹理查找
当片段着色器计算纹理坐标而不是使用传递给着色器的未修改的纹理坐标时,会发生动态纹理查找(也称为依赖纹理读取)。在OpenGL ES 3.0功能的硬件上,无需性能成本支持相关的纹理读取;在其他设备上,依赖纹理读取可以延迟纹理数据的加载,从而降低性能。当着色器没有依赖纹理读取时,图形硬件可能在着色器执行之前预取纹理数据,隐藏访问内存的一些延迟。
清单10-7显示了一个计算新纹理坐标的片段着色器。相反,本示例中的计算可以轻松地在顶点着色器中执行。通过将计算移动到顶点着色器并直接使用顶点着色器的计算的纹理坐标,您可以避免依赖的纹理读取
注意:这可能不是很明显,但是纹理坐标上的任何计算都计为依赖纹理读取。例如,将多组纹理坐标包装到单个变化参数中,并使用旋转命令提取坐标仍然会导致依赖的纹理读取
Listing10-7
varying vec2 vTexCoord;
uniform sampler2D textureSampler;
void main()
{
vec2 modifiedTexCoord = vec2(1.0 - vTexCoord.x, 1.0 - vTexCoord.y);
gl_FragColor = texture2D(textureSampler, modifiedTexCoord);
}
获取可编程混合的帧缓冲区数据
传统的OpenGL和OpenGL ES实现提供了一个固定功能的混合阶段,如图10-1所示。在发出绘图调用之前,您可以从一组固定参数中指定混合操作。在您的片段着色器输出像素的颜色数据之后,OpenGL ES混合阶段将读取目标帧缓冲区中相应像素的颜色数据,然后根据指定的混合操作组合两个数据以产生输出颜色。
10-1.png在iOS 6.0及更高版本中,您可以使用EXT_shader_framebuffer_fetch扩展来实现可编程的混合和其他效果。您的片段着色器不是提供要由OpenGL ES混合的源颜色,而是读取与正在处理的片段相对应的目标帧缓冲区的内容。您的片段着色器可以使用您选择的任何算法来产生输出颜色,如图10-2所示。
10-2.png此扩展支持许多高级渲染技术:
- 附加混合模式。通过定义您自己的GLSL ES功能来组合源和目标颜色,您可以使用OpenGL ES固定功能混合阶段不可能实现混合模式。例如,清单10-8实现了流行图形软件中的Overlay和Difference混合模式。
- 后处理效果。渲染场景后,您可以使用片段着色器绘制全屏四面体,读取当前片段颜色并将其转换为产生输出颜色。清单10-9中的着色器可以用于将场景转换为灰度级的技术。
- 非彩色片段操作。帧缓冲区可能包含非彩色数据。例如,延迟着色算法使用多个渲染目标来存储深度和正常信息。您的片段着色器可以从一个(或多个)渲染目标读取这些数据,并使用它们在另一个渲染目标中产生输出颜色。
这些效果在没有帧缓冲区提取扩展的情况下是可能的 - 例如,灰度转换可以通过将场景渲染成纹理,然后使用该纹理绘制全屏四面体,并将片段着色器转换为灰度级。但是,使用此扩展通常会导致更好的性能。
要启用此功能,您的片段着色器必须声明它需要EXT_shader_framebuffer_fetch扩展名,如清单10-8和清单10-9所示。用于实现此功能的着色器代码在OpenGL ES着色语言(GLSL ES)的版本之间不同。
在GLSL ES 3.0中使用帧缓冲区提取
在GLSL ES 3.0中,您可以使用用限定符声明的用户定义的变量进行片段着色器输出。如果使用inout限定符声明片段着色器输出变量,则在片段着色器执行时将包含帧缓冲区数据。清单10-9说明了使用inout变量的灰度后处理技术
Listing10-9
#version 300 es
#extension GL_EXT_shader_framebuffer_fetch : require
layout(location = 0) inout lowp vec4 destColor;
void main()
{
lowp float luminance = dot(vec3(0.3, 0.59, 0.11), destColor.rgb);
destColor.rgb = vec3(luminance);
}
在顶点着色器中使用更大内存缓冲区的纹理
在iOS 7.0及更高版本中,顶点着色器可以从当前绑定的纹理单位读取。使用这种技术,您可以在顶点处理期间访问更大的内存缓冲区,从而实现一些高级渲染技术的高性能。例如:
- 位移映射用默认顶点位置绘制网格,然后从顶点着色器中的纹理读取以改变每个顶点的位置。清单10-10显示了使用这种技术从灰度高度图纹理生成三维几何。
- 实例绘图。如使用实例绘图中最小化绘图调用所述,实例绘制可以显着减少渲染包含许多类似对象的场景时的CPU开销。然而,向顶点着色器提供每个实例的信息可能是一个挑战。纹理可以存储许多实例的广泛信息。例如,您可以通过从仅描述简单多维数据集的顶点数据绘制数百个实例来呈现广阔的城市景观。对于每个实例,顶点着色器可以使用gl_InstanceID变量从纹理采样,获得转换矩阵,颜色变化,纹理坐标偏移和高度变化以应用于每个建筑物
Listing10-10
attribute vec2 xzPos;
uniform mat4 modelViewProjectionMatrix;
uniform sampler2D heightMap;
void main()
{
// Use the vertex X and Z values to look up a Y value in the texture.
vec4 position = texture2D(heightMap, xzPos);
// Put the X and Z values into their places in the position vector.
position.xz = xzPos;
// Transform the position vector from model to clip space.
gl_Position = modelViewProjectionMatrix * position;
}
您还可以使用统一的数组和统一的缓冲区对象(在OpenGL ES 3.0中)向顶点着色器提供批量数据,但顶点纹理访问提供了几个潜在的优势。您可以在纹理中存储比数据或统一缓冲区对象中更多的数据,并且可以使用纹理包装和过滤选项来插入纹理中存储的数据。此外,您可以渲染到纹理,利用GPU来生成数据,以便在稍后的顶点处理阶段使用。
要确定顶点纹理采样是否在设备上可用(以及顶点着色器可用的纹理单元数量),请在运行时检查MAX_VERTEX_TEXTURE_IMAGE_UNITS限制的值。 (请参阅验证OpenGL ES功能。)
网友评论