我的OpenGL学习进阶之旅OpenGL ES 着色语言 (下)
Posted 欧阳鹏
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我的OpenGL学习进阶之旅OpenGL ES 着色语言 (下)相关的知识,希望对你有一定的参考价值。
【我的OpenGL学习进阶之旅】OpenGL ES 着色语言 (下)
回顾【我的OpenGL学习进阶之旅】OpenGL ES 着色语言 (上)
在上一篇博客【我的OpenGL学习进阶之旅】OpenGL ES 着色语言 (上)中,我们介绍了如下内容:
- 一、OpenGL ES 着色语言基础知识
- 二、着色器版本规范
- 三、变量和变量类型
- 3.1 标量类型
- 3.2 向量类型
- 3.3 矩阵类型
- 3.4 采样器
- 四、变量构造器
- 4.1 如何使用构造器初始化和转换标量值
- 4.2 如何使用构造器转换和初始化向量数据类型
- 4.3 如何使用构造器转换和初始化矩阵数据类型
- 五、向量和矩阵分量
- 5.1 使用
“.” 运算符
- 5.2 使用
数组下标“[]”
- 5.1 使用
- 六、常量
- 七、结构
- 八、数组
- 九、运算符
这篇博客,我们继续来介绍OpenGL ES 着色语言的其他内容。
十、函数
函数的声明方法和C语言中相同。如果函数在定义前使用,则必须提供原型说明。
OpenGL ES 着色语言函数和C语言函数的最明显不同之处在于函数参数的传递方法。
OpenGL ES 着色语言提供特殊的限定符,定义函数是否可以修改可变参数;
10.1 OpenGL ES 着色语言限定符
下表展示这些限定符。
限定符 | 描述 |
---|---|
in | (没有指定时的默认限定符) 这个限定符指定参数按值传送,函数不能修改 |
inout | 这个限定符规定变量按照引用传入函数,如果该值被修改,它将在函数退出后变化 |
out | 这个限定符表示该变量的值不被传入函数,但是在函数返回时将被修改 |
下面提供一个函数声明的实例,这个例子说明了参数限定符的用法。
vec4 myFunc(inout float myFloat, // inout parameter
out vec4 myVec4, // out parameter
mat4 myMat4); // in parameter
下面的函数定义示例是一个计算基本漫射光线的简单函数:
vec4 diffuse(vec3 normal, vec3 light,vec4 baseColor)
return baseColor * dot( normal , light );
10.2 函数不能递归
关于OpenGL ES 着色语言中的函数还要注意一点:函数不能递归。
这一限制的原因是某些实现通过把函数代码真正地内嵌到GPU生成的最终程序来实施函数调用。着色语言有意地构造为允许这种内嵌式实现,以支持没有堆栈的GPU。
十一、内建函数
OpenGL ES 着色语言中最强大的功能之一是该语言中提供的内建函数。
下面的例子是一些用于在片段着色器中计算基本反射照明的着色器代码:
float nDotL = dot ( normal, light);
float rDotV = dot ( viewDir, (2.0 * normal) * nDotL - light );
float specular = specularColor * pow( rDotV, specularPower );
如你所见,这个着色器代码块使用了内建函数 dot
计算两个向量的点积,用内建函数 pow
计算标量的幂次。
这只是两个简单的例子,OpenGL ES 着色语言 有许多内建函数,可以处理通常在着色器中进行的各种计算任务。
那些完整的内建函数参考《OPENGL ES 3.0编程指南 原书第2版 》 附录B
。
十二、控制流语句
12.1 if-then-else语句
OpenGL ES 着色语言中的控制流语句的语法类似于C语言。简单的 if-then-else逻辑测试可以用与C语言相同的语法完成。
例如:
if( color.a < 0.25)
color *= color.a;
else
color = vec4(0.0);
条件语句中测试的表达式求出的必须是一个布尔值。也就是说,测试必须基于一个布尔值或者某些得出布尔值的表达式。这是条件在OpenGL ES 着色语言中的基本表达方式。
12.2 while和do-while循环
除了基本的 if-then-else 语句之外,还可以编写 while和do-while循环。
在OpenGL ES 2.0中,循环的使用有非常严格的管控规则。本质上,只有编译器能够展开的循环才得到支持。
这些限制在OpenGL ES 3.0中不复存在。人们期望GPU硬件为循环和流控提供支持,因此循环得到完全的支持。
这并不是说循环在性能上没有什么影响。在大部分GPU架构中,顶点或者片段并行批量执行。GPU通常要求一个批次中的所有片段或者顶点计算流控制语句中的所有分支(或者循环迭代)。如果批次中的顶点或者片段执行不同的路径,则批次中的所有其他顶点\\片段通常都必须也执行该路径。批次的大小特定于GPU,往往需要进行剖析,以确定在特定架构中使用流控的性能意义。
但是,经验法则是,应该尝试限制跨顶点/片段的扩散性流控或者循环迭代的使用。
十三、统一变量
13.1 统一变量简介
在前面的博客【我的OpenGL学习进阶之旅】统一变量和属性中我们介绍了统一变量。
OpenGL ES 着色语言中的变量类型限定符之一是统一变量。
统一变量存储应用程序通过 OpenGL ES 3.0 API 传入着色器的只读值,对于保存着色器所需的所有数据类型(如变换矩阵、照明参数和颜色)都很有用。
本质上,一个着色器的任何参数在所有顶点或者片段中都应该以统一变量的形式传入。在编译时已知值的变量应该是常量,而不是统一变量,这样可以提高效率。
统一变量在全局作用域中声明,只需要统一限定符。
13.2 统一变量例子
下面是统一变量的一些例子:
uniform mat4 viewProjMatrix;
uniform mat4 viewMatrix;
uniform mat3 lightPosition;
13.3 统一变量注意事项
之前的博客我们描述了应用程序如何将统一变量加载到着色器。
还要注意,统一命令的命名空间在顶点着色器和片段着色器中都是共享的。也就是说,如果顶点和片段着色器一起链接到一个程序对象,它们就会共享同一组统一变量。
因此,如果在顶点着色器和片段着色器中都声明一个统一变量,那么两个声明必须匹配。 应用程序通过API加载统一变量时,它的值在顶点和片段着色器中都可用。
13.4 统一变量数量限制
统一变量通常保存在硬件上,这个区域被称作“常量存储”,是硬件中为存储常量值而分配的特殊空间。
因此常量存储的大小一般是固定的,所以程序中可以使用的统一变量数量受到限制。
查询统一变量数量限制的方法
- 这种限制可以通过读取内建变量
gl_MaxVertexUniformVectors
和gl_MaxFragmentUniformVectors
的值来确定。 - 或者用
glGetintegerv
查询GL_MAX_VERTEX_UNIFORM_VECTORS
或GL_MAX_FRAGMENT_UNIFORM_VECTORS
OpenGL ES 3.0实现必须提供至少256个顶点统一向量和224个片段统一向量,但是也可以提供更多。
十四、统一变量块
14.1、统一变量块简介
在前面的博客【我的OpenGL学习进阶之旅】统一变量和属性,我们介绍了统一变量缓冲区对象的概念。
这里复习一些,统一变量缓冲区对象可以通过一个缓冲区对象支持统一变量数据的存储。统一变量缓冲区对象在某些条件下比单独的统一变量有更多的优势。
例如:
- 利用统一变量缓冲区对象,统一变量缓冲区数据可以在多个程序中共享,但只需要设置一次。
- 此外,统一变量缓冲区对象一般可以存储更大量的统一变量数据
- 最后,在统一缓冲区对象之间切换比一次单独加载一个统一变量更高效。
统一缓冲区对象可以在OpenGL ES 着色语言中通过应用统一变量块使用。
下面是统一变量块的例子:
uniform TransformBlock
mat4 matViewProj;
mat3 matNormal;
mat3 matTexGen;
;
上述代码声明一个名为TransformBlock
且包含3个矩阵的统一变量块。
名称TransformBlock
将供程序使用,作为统一缓冲区对象函数glGetUniformBlockIndex
中的blockName
参数。
14.2 统一变量块声明中的变量在着色器中的访问
统一变量块声明中的变量在着色器中都可用访问,就像常规形式声明的变量一样。
例如,TransformBlock
中声明的matViewProj
的访问方法如下:
#version 300 es
uniform TransformBlock
mat4 matViewProj;
mat3 matNormal;
mat3 matTexGen;
;
layout(location = 0) in vec4 a_position;
void main()
gl_Position = matViewProj * a_position;
14.2 统一变量块的布局限定符
一些可选的布局限定符可用于指定支持统一变量块的统一缓冲区对象在内存中的布局方式。
布局限定符可用提供给单独的统一变量块,或者用于所有统一变量块。 在全局作用域内,为所有统一变量块设置默认布局的方式如下:
layout(shared, column_major) uniform; // default if not specified
layout(packed, row_major) uniform;
单独的统一变量块也可以通过覆盖全局作用域上的默认设置来设置布局。
此外,统一变量块中的单独统一变量也可以指定布局限定符,如下所示:
layout(std140) uniform TransformBlock
mat4 matViewProj;
layout(row_major) mat3 matNormal;
mat3 matTexGen;
;
下表列出了可以用于统一变量块的所有布局限定符。
包括:
shared
packed
std140
row_major
column_major
十五 顶点和片段着色器输入/输出
15.1 顶点输入变量
OpenGL ES 着色语言的另外一个特殊变量类型是顶点输入(或者属性)变量。
顶点输入变量用于指定顶点着色器中每个顶点的输入,用in关键字
指定。它们通常存储位置、法线、纹理坐标和颜色这样的数据。 这里的关键是理解顶点输入是为绘制的每个顶点指定的数据。
下面例子是具有位置和颜色顶点输入变量的顶点着色器样板。
uniform mat4 u_matViewProjection;
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec3 a_color;
out vec3 v_color;
void main(void)
gl_Position = u_matViewProjection * a_position;
v_color = a_color;
这个着色器的两个输入变量a_position
和a_color
的数据由应用程序加载。本质上,应用程序将为每个顶点创造一个顶点数组,该数组包含位置和颜色。
注意例子中的顶点输入变量之前使用了layout
限定符。这种情况下的布局限定符用于指定顶点属性的索引。
布局限定符是可选的,如果没有指定,链接程序将自动为顶点输入变量分配位置。
15.2 可输入顶点着色器的属性变量数目限制
和统一变量一样,底层硬件通常在可输入顶点着色器的属性变量数目上有所限制。
查询OpenGL ES 实现 支持的最大属性数量的方法有:
- OpenGL ES 实现 支持的最大属性数量由内建变量
gl_MaxVetexAttribs
给出。 - 或者使用
glGetIntegerv
查询GL_MAX_VERTEX_ATTRIBS
OpenGL ES 3.0 实现可支持的最小属性为16个。 不同的实现可以支持更多变量,但是如果想要编写保证能在任何OpenGL ES 3.0 实现上运行的着色器,则应该将属性限制为不多于16个。
15.3 顶点着色器的输出变量
来自顶点着色器的输出变量有out
关键字指定。 上面的例子中,v_color
变量被声明为输出变量,其内容从a_color
输入变量中复制而来。
每个顶点着色器将在一个或者多个输出变量中输出需要传递给片段着色器的数据。 然后,这些变量也会在片段着色器中声明为in
变量(类型相符),在光栅化阶段中对图元进行线性插值。
例如,片段着色器与上面例子中的顶点输出v_color
变量相匹配的输入声明如下:
in vec3 v_color
注意,与顶点着色器输入不同,顶点着色器输出/片段着色器输入变量 不能有布局限定符。 OpenGL ES 实现自动选择位置。
15.4 顶点着色器输出/片段着色器输入的数量限制
与统一变量和顶点输入属性相同,底层硬件通常限制顶点着色器输出/片段着色器输入(在硬件上,这些变量通常被称作 插值器)的数量。
15.4.1 查询OpenGL ES 实现支持的顶点着色器输出的数量的方法
查询OpenGL ES 实现支持的顶点着色器输出的数量的方法有:
- OpenGL ES 实现 支持的最大属性数量由内建变量
gl_MaxVetexOutputVecors
给出。 - 使用
glGetIntegerv
查询GL_MAX_VERTEX_OUTPUT_COMPONENTS
将提供总分量值数量,而非向量数量
OpenGL ES 3.0 实现可支持的最小顶点输出向量数为16.
15.4.2 查询OpenGL ES 实现支持的片段着色器输入的数量的方法
查询OpenGL ES 实现支持的片段着色器输入的数量的方法有:
- OpenGL ES 实现 支持的最大属性数量由内建变量
gl_MaxFragmentInputVecors
给出。 - 使用
glGetIntegerv
查询GL_MAX_FRAGMENT_INTPUT_COMPONENTS
将提供总分量值数量,而非向量数量
OpenGL ES 3.0 实现可支持的最小片段输入向量数为15.
下面的例子,是具有匹配的输出/输入声明的顶点着色器和片段着色器
- Vertex shader
#version 300 es
uniform mat4 u_matViewProjection;
// Vertex shader inputs
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec3 a_color;
// Vertex shader outputs
out vec3 v_color;
void main(void)
gl_Position = u_matViewProjection * a_position;
v_color = a_color;
- Fragment shader
#version 300 es
precision mediump float;
// Input from vertex shader
in vec3 v_color;
// Output of fragment shader
layout(location = 0) out vec4 o_fragColor;
void main()
o_fragColor = vec4(v_color,1.0);
在上面的例子中,片段着色器包含输出变量o_fragColor
的定义
layout(location = 0) out vec4 o_fragColor;
片段着色器将输出一个或者多个颜色。
在典型的情况下,我们只渲染一个颜色缓冲区,在这个时候,布局限定符是可选的(假定输出变量进入位置0)。
但是,当渲染到多个渲染目标(MRT)时,我们可以使用布局限定符指定每个输出前往的渲染目标。对于这种典型的情况,在片段着色器中会有一个输出变量,该值将是传递给管线逐片段操作部分的输出颜色。
十六、插值限定符
在上一个例子中,我们声明了自己的顶点着色器输出和片段着色器输入,没有使用任何限定符。
在没有限定符时,默认的插值行为是执行平滑着色。也就是说,来自顶点着色器的输出变量在图元中线性插值,片段着色器接收线性插值之后的数值作为输入。
16.1 平滑着色 smooth
我们可以明确地请求平滑着色,而不是依赖默认行为。在这种情况下,输出\\输入情况如下:
- Vertex shader
// Vertex shader outputs
smooth out vec3 v_color;
- Fragment shader
// Input from vertex shader
smooth in vec3 v_color;
16.2 平面着色 flat
OpenGL ES 3.0 还引入了另一种插值 ------ 平面着色。
在平面着色中,图元的值没有进行插值,而是将其中一个顶点视为 驱动顶点(Provoking Vertex,取决于图元类型),该顶点的值被用于图元中的所有片段。
我们可以声明如下的平面着色输出\\输入:
- Vertex shader
// Vertex shader outputs
flat out vec3 v_color;
- Fragment shader
// Input from vertex shader
flat in vec3 v_color;
16.3 centroid关键字
可以用 centroid
关键字 在插值器中添加另一个限定符。 质心采样(centroid sampling)这里不做介绍。本质上,使用多重采样渲染时, centroid
关键字可用于强制插值发生在被渲染图元内部(否则,在图元的边缘可能出现伪像)。
现在,我们简单地介绍使用质心采样的输出\\输入变量的方法。
- Vertex shader
// Vertex shader outputs
smooth centroid out vec3 v_color;
- Fragment shader
// Input from vertex shader
smooth centroid in vec3 v_color;
十七、预处理和指令
我们尚未提及一个OpenGL ES着色语言概念是 预处理器。
17.1 使用如下指令定义宏和条件测试
OpenGL ES着色语言配备一个预处理器,遵循许多标准C++预处理器的定义。可以使用如下指令定义宏和条件测试
#define
#undef
if
ifdef
ifndef
else
elif
endif
注意,宏不能定义为带有参数(C++的宏可以这样)。 #if、#else和#elif指令
可以使用defind
测试来查看宏是否已经定义。
17.2 预先定义的宏
下面的宏是预先定义的,接下来将作说明:
__LINE__
Replaced with the current line number in shader__FILE__
Always 0 in OpenGL ES 3.0__VERSION__
The OpenGL ES shading language version (e.g, 300)GL_ES
This will be defined for ES shaders to a value of 1
17.3 其他的指令
#error
指令
将会导致在着色器编译时出现编译错误,并且在信息日志中放入对应的消息。#progma
指令
用于为编译器指定特定于实现的指令。#extension
指令
用于启用和设置扩展的行为。
当供应商扩展OpenGL ES 着色器语言时,它们将创建一个语言扩展规范。着色器必须命令编译器是否允许使用扩展,如果不允许,应该采取什么行动,这些用 #extension
指令 完成。
下面是 #extension
指令 的一般格式:
// set behavior for an extension
#extension extension_name : behavior
# set behavior for ALL extensions
#extension all: behavior
第一个参数是扩展的名称 或者 表示该行为适用于所有扩展的all。 该行为有4个可能的选项,如下表所示:
举个例子,假定你希望预处理器在NVIDIA阴影采样器立方体扩展不受支持时产生警告(而且,你希望在该扩展受到支持时着色器得到处理)。为此,你将在着色器开始处添加如下语句:
#extension GL_NV_shadow_samplers_cube : enable
十八 统一变量和插值器打包
前面我们介绍了,底层硬件中可用于每个变量存储的资源是固定的。
统一变量通常存储在所谓的“常量存储”中,这可以看作向量的物理数组。
顶点着色器输出\\片段着色器输入一般保存在插值器中,这通常也保存为一个向量数组。
你可能已经注意到,着色器可能声明各种类型的统一变量和着色器输入\\ 输出,包括 标量、各种向量分量和矩阵。但是,这些变量声明如何映射到硬件上的可用物理空间呢? 换言之,如果一个OpenGL ES 3.0实现支持16个顶点着色器输出向量,那么物理存储实际上是如何使用的呢?
18.1 打包规则
在OpenGL ES 3.0中,这个问题通过打包规则处理,该规则定义定义插值器和统一变量映射到物理存储空间的方式。
打包规则基于物理存储空间被组织为一个每个存储位置4列(每个向量分量一列)和1行的网格的概念。
打包规则寻求打包变量,使生成代码的复杂度保持不变。换言之,打包规则不进行重排序操作(这种操作需要编译器生成合并未打包数据的额外指令),而是试图在不对运行时性能产生负面影响的情况下,优化物理地址空间的使用。
我们来看一组统一变量声明的例子,看看如何打包它们:
uniform mat3 m;
uniform float f[6];
uniform vec3 v;
18.1.1 完全不进行打包
如果完全不进行打包,你可能发现许多常量存储空间将被浪费。
矩阵m
将占据3行,数据f
占据6行,向量v
占据1行,共需要10行才能存储这些变量。
下表展示了不进行任何打包的结果。
18.1.2 使用打包规则
利用打包规则,这些变量将被组织以打包到下表所示的网格中。
在使用打包规则时,只需要6个物理常量位置。
你会注意到,数组f的元素会跨越行的边界,原因是GPU通常会按照向量位置索引对常量存储进行索引。
打包必须使数组跨越行便捷,这样索引才能够起作用。
所有打包对OpenGL ES着色语言的用户都是完全透明的,除了一个细节:打包影响统一变量和顶点着色器输出\\片段着色器输入的计数方式。
注意:如果你想要编写保证能够在所有OpenGL ES 3.0 实现上运行的着色器,就不应该使用打包之后超过最小运行存储大小的统一变量或者插值器。
因此,了解打包非常重要,这样你才能编写在任何OpenGL ES 3.0 实现上都不超过最小允许存储的可移植着色器。
十九 精度限定符
精度限定符使着色器创作者可以指定着色器变量的计算精度。
变量可以声明为低、中或者高精度。
这些限定符用于提示编译器允许在较低的范围和精度上执行变量计算。在较低精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。
当然,这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。
注意,OpenGL ES 规范中没有规定底层硬件中必须支持多种精度,所以某个OpenGL ES 实现在最高精度上进行所有运算并简单地忽略限定符是完全正常的。 不过,在某些实现上,使用较低的精度可能带来好处。
19.1 精度限定符
精度限定符可用用于指定任何基于浮点数或者整数的变量的精度。
指定精度的关键字是
lowp,mediump和 highp
。
下面是一些带有精度限定符的声明示例:
highp vec4 position;
in lowp vec4 color;
mediump float specularExp;
19.2 默认精度
除了精度限定符之外,还有默认精度的概念。也就是说,如果变量声明时没有使用精度限定符,它将拥有该类型的默认精度。
默认精度限定符在顶点或者片段着色器的开头用如下语法指定:
precision highp float;
precision mediump int;
为 float 类型指定的精度 将作用于所有基于浮点值的变量的默认精度。
同样,为int指定的精度 将作用于所有基于整数的变量的默认精度。
- 顶点着色器的默认精度规则
在顶点着色器中,如果没有指定默认精度,则int和float的默认精度都是highp。 - 片段着色器的默认精度规则
片段着色器中,浮点值没有默认的精度值。每个着色器必须声明一个默认的float精度,或者为每个float变量指定精度。
最后要注意的是,精度限定符指定的精度有特定于实现的范围和精度。
二十 不变性
OpenGL ES 着色语言中引入的 invariant 关键字可以用于任何可变的顶点着色器输出。
20.1 不变性是什么意思?为什么它很必要呢?
问题在于着色器需要编译,而编译器可能进行导致指令重新排序的优化。这种指令重排意味着两个着色器之间的等价计算不能保证产生完全相同的结果。 这种不一致性在多遍着色器特效时尤其可能称为问题,在这种情况下,相同的对象用Alpha混合绘制在自身上方。如果用于计算输出位置的数值的精度不完全一样,精度差异就会导致伪像。这个问题通常表现为“深度冲突(Z fighting)”,每个像素的Z(深度)精度差异导致不同遍着色相互之间有微小的偏移。
下面例子直观的展现了进行多遍着色时不变性的重要性。
图中的圆环对象用两遍绘制:片段着色器在第一遍中计算反射,第二遍中计算环境光线和漫射光。顶点着色器不使用不变性,所以精度的小差异会导致深度冲突,如下图所示:
使用不变性的同一多遍顶点着色器产生如下图所示的正确图像。
引入不变性是为着色器编写者提供了一种途径来规定用于计算输出的相同计算的值必须相同(或者不变)。
20.2 不变性关键字 invariant
invariant 关键字可以用于变量声明,或者用于已经声明的变量。
下面是一些例子:
invariant gl_Position;
invariant texCoord;
一旦某个输出变量声明了不变性,编译器便保证相同的计算和着色器输入条件下结果是相同。
例如,两个顶点着色器通过将试图投影矩阵和输入位置相乘计算输出位置,你可以保证这些位置不变。
#version 300 es
uniform mat4 u_ViewProjMatrix;
layout(location = 0) in vec4 a_vertex;
invariant gl_Position;
void main()
// Will be the same value in all shaders with
// the same u_ViewProjMatrix and a_vertex
gl_Position = u_ViewProjMatrix * a_vertex;
20.3 用#pragma指令让所有变量全部不变
也可以用#pragma指令让所有变量全部不变:
#pragma STDGL invariant(all)
20.4 警告
警告: 因为编译器需要保证不变性,所以可能限制它所做的优化。因此,invariant限定符应该只在必须时使用;否则可能导致性能下降。
由于这个原因,全局启用不变性的#pragma指令只应该在不变性对应所有变量都必须的时候使用。
还要注意,虽然不变性表示在指定GPU上的计算会得到相同的结果,但是不意味着计算在任何OpenGL ES 实现之间保持不变。
以上是关于我的OpenGL学习进阶之旅OpenGL ES 着色语言 (下)的主要内容,如果未能解决你的问题,请参考以下文章
我的OpenGL学习进阶之旅学习OpenGL ES 3.0 的实战 Awsome Demo (中)
我的OpenGL学习进阶之旅学习OpenGL ES 3.0 的实战 Awsome Demo (中)
我的OpenGL学习进阶之旅学习OpenGL ES 3.0 的实战 Awsome Demo (上)
我的OpenGL学习进阶之旅学习OpenGL ES 3.0 的实战 Awsome Demo (上)