【OpenGL ES】着色语言GLSL

OpenGL ES 3.0顶点着色器和片段着色器的第一行总是声明着色器版本(如#version 300 es),通知着色器编译器预期在着色器中出现的语法和结构,检查着色器语法,默认为OpenGL ES着色语言的1.00版本,用于OpenGL ES 2.0,对于OpenGL ES 3.0,版本号为3.00,增加了许多新功能,包括非方矩阵、全整数支持、插值限定符、统一变量块、布局限定符、新的内建函数、全循环、全分支支持以及无限的着色器指令长度等。下面介绍OpenGL ES着色语言3.00版本的用法。

1、数据类型

着色语言中的变量必须以某个数据类型作为声明(如mat4 mvp;),这些数据类型如下所示。

标量:float、int、uint、bool
浮点向量:float、vec2、vect3、vec4
整数向量:int、ivec2、ivec3、ivec4
无符号整数向量:uint、uvec2、uvec3、uvec4
布尔向量:bool、bvec2、bvect3、bvec4
浮点矩阵:mat2(mat2x2)、mat2x3、mat2x4、mat3x2、mat3(mat3x3)、mat3x4、mat4x2、mat4x3、mat4(ma4x4)

2、变量及变量类型

着色语言中的变量类型有着严格的要求,不允许像C/C++那样进行隐式类型转换,也就是说不同类型的变量之间不能进行简单的赋值和运算,必须使用对应类型的构造函数(类型名)进行显式类型转换,但用法比较灵活,变量的值可以在变量声明时初始化或者在后面赋值,下面举几个简单的例子。

float myFloat = 1.0;
float myFloat2 = 1; // error
bool myBool = true;
int myInt = 0;
int myInt2 = 0.0; // error
myFloat = float(myBool); // bool > float
myFoat = float(myInt); // int > float
myBool = bool(myInt); // int > bool
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0};
vec3 myVec3 = vec3(1.0, 0.0, 0.5); // myVec3 = {1.0, 0.0, 0.5};
vec3 temp = vec3(myVec3); // temp = myVec3
vec2 myVec2 = vec2(myVec3); // myVec2 = {myVec3.x, myVec3.y};
myVec4 = vec4(myVec2, temp); // myVec4 = {myVec2.x, myVec2.y, temp.x, temp.y};
mat4 myMat4 = mat4(1.0); // 4x4单位矩阵
mat3 myMat3 = mat3(1.0, 0.0, 0.0, // 列优先,第一列
   0.0, 1.0, 0.0, // 第二列
   0.0, 0.0, 1.0); // 第三列
const float zero = 0.0;
const float pi = 3.14159;
const vec4 read = vec4(1.0, 0.0, 0.0, 1.0);
const mat4 identity = mat4(1.0);
float floatArray[4] = float[4](1.0, 1.0, 1.0, 1.0);
int intArray[4] = int[](1, 1, 1, 1);
vec2 vec2Array[2] = vec2[2](vec2(1.0), vec2(1.0));
struct fogStruct
{
vec4 color;
float begin;
float end;
} fogVar;
fogVar = fogStruct(vec4(1.0, 0.0, 0.0, 1.0), // color
     0.5, // begin
     2.0); // end
vec4 color = fogVar.color;
float begin = fogVar.begin;
float end = fogVar.end;

常量(只读变量)在声明时使用const限定符并初始化。数组在声明时指定数组大小,使用数组构造函数(类型名加一对方括号)进行初始化,数组构造函数可以不指定大小,但数组的元素个数需保持一致。结构struct是一种自定义类型,用法同C语言一样,不同的是使用结构构造函数(结构类型名)进行初始化。向量的各个分量有两种方法,一种是使用数组下标[index],index表示向量分量的索引,另一种是使用运算符.,后面跟着向量分量的名字,名字有三种表示形式,坐标形式的{x, y, z, w},颜色形式的{r, g, b, a},纹理形式的{s, t, p, q},三种形式的各个分量依次与向量的各个分量依次对应,如x、r、s对应于向量的第一个分量,w、a、q对应于向量的第四个分量,而且这三种形式不能混用,下面举几个简单的例子。

vec3 myVec3 = vec3(0.0, 1.0, 2.0); // myVec3 = {0.0, 1.0, 2.0};
vec3 temp;
temp = myVec3.xyz; // temp = {0.0, 1.0, 2.0};
temp = myVec3.xxx; // temp = {0.0, 0.0, 0.0};
temp = myVec3.zyx; // temp = {2.0, 1.0, 0.0};

3、运算符

* 乘
/ 除
% 取模
+ 加
- 减
++ 递增
-- 递减
= 赋值
+= -= *= /= 算术赋值
== != < > <= >= 比较运算符
&& 逻辑与
^^ 逻辑异或
|| 逻辑或
<< >> 移位
& ^ | 按位与、异或、或
?: 选择
, 序列

着色语言支持以上运算符,对于二元运算符*/+-,变量的类型必须是浮点或者整数,但*可以在浮点、向量和矩阵之间进行运算,除了==!=之外,比较运算符<<=>>=只能用于标量值,要比较向量,可以使用内建函数,逐个分量进行比较,下面举几个简单的例子。

float myFloat;
vec4 myVec4;
mat4 myMat4;
myVec4 = myVec4 * myFloat;
myVec4 = myVec4 * myVec4;
myMat4 = myMat4 * myFloat;
myMat4 = myMat4 * myVec4;
myMat4 = myMat4 * myMat4;

4、函数

着色语言提供了许多内建函数,如dot计算两个向量的点积,pow计算标量的幂次等,可以处理通常在着色器中进行的各种计算任务,当然也可以像C语言一样自定义函数,最明显的不同之处在于函数参数的传递方法,有三个参数限定符in、inout、out,其中默认限定符in表示参数按值传递,不可修改,inout表示参数按引用传递,可以修改,out表示参数不传入,但可以修改,下面是一个自定以函数的声明。

vec4 myFunc(inout float myFloat, // inout
                      out vec4 myVec4, // out
                      mat4 myMat4); // default in

函数用法有一个限制,不能递归,这一限制的原因是,某些实现通过把函数代码真正地内嵌到为GPU生成的最终程序来实施函数调用,着色语言有意地构造为允许这种内嵌式实现,以支持没有堆栈的GPU。

5、控制流和预处理器

着色语言支持if-else、while、do-while,条件语句中测试表达式求出的必须是一个布尔值。在大部分GPU架构中,顶点或者片段并行批量执行,GPU通常要求一个批次中的所有顶点或者片段计算控制流语句中的所有分支或者循环迭代,如果批次中的顶点或者片段执行不同的路径,则批次中的其它顶点、片段通常都必须也执行该路径,批次的大小特定于GPU,往往需要进行剖析,以确定在特定架构中使用控制流的性能意义,但是,经验法则是,应该尝试限制跨顶点、片段的扩散性控制流和循环迭代的使用。下面是一个if-else控制流语句。

if (color.a < 0.25)
{
color *= color.a;
}
else
{
color = vec4(0.0);
}

着色语言支持如下预处理器指令:

#define
#undefine
#ifdef
#ifndef
#if
#elif
#else
#endif
#error
#pragma
#extension

预处理器指令类似于C语言,但也有不同之处。定义宏时不能带有参数,if、elif和else指令可以使用defind测试来查看宏是否已经定义,着色语言内置宏包括__LINE__(着色器中的行号)、__FILE__(OpenGL ES 3.0中为0)、__VERSION__(着色语言版本,如300)、GL_ES(1)。error指令将会导致在着色器编译时出现编译错误,并在信息日志中放入对应的消息。pragma指令用于为编译器指定特定于实现的指令。extension用于启用和设置扩展的行为,格式为#extension extension_name : behavior,extension_name为一个扩展名或者是影响全部扩展的默认关键字all,behavior可以是关键字enabledisablerequirewarn

6、uniform

uniform是着色语言中的变量类型限定符,存储应用程序通过OpenGL ES API传入着色器的只读值,统一变量在全局作用域中声明,其命名空间在顶点着色器和片段着色器中都是共享的,也就是说,如果顶点和片段着色器一起链接到一个程序对象,它们就会共享同一组统一变量,因此,如果在顶点着色器和片段着色器中都声明一个统一变量,那么两个声明必须匹配,应用程序通过API加载统一变量时,它的值在顶点和片段着色器中都可用。统一变量通常保存在硬件中,这个区域通常被称作常量存储,不同于编译时已知道值的常数变量,而是硬件中为存储常量值而分配的特殊空间,因为常量存储的大小一般是固定的,所以程序中可以使用的统一变量数量受到限制,这种限制可以通过读取内建变量gl_MaxVertexUniformVectors和gl_MaxFragmentUniformVectors的值来确定,或者用glGetIntegerv查询GL_MAX_VERTEX_UNIFORM_VECTORS或GL_MAX_FRAGMENT_UNIFORM_VECTORS。OpenGL ES 3.0实现必须提供至少256个顶点统一变量和224个片段统一变量。下面举几个简单的例子。

uniform mat4 viewProjMatrix;
uniform mat4 viewMatrix;
uniform vec3 lightPosition;

uniform还有一个更高级的概念,统一变量缓冲区对象,比单独的统一变量更高效,在着色语言中对应于统一变量块,下面是一个统一变量块,块名称TransformBlock供应用程序使用,如作为glGetUnformBlockIndex的参数,块中的变量在着色器中都可以直接访问,就像常规形式声明的变量一样。

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

7、布局

先看下面的例子:

layout(location = 0) in vec4 a_position;
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;
};

布局layout是可选的限定符,指定属性变量或统一变量(块)在内存中的布局方式,有多种设置方式,其中location用于指定顶点输入和片段输出的索引;shared表示多个着色器或者多个程序中统一变量块的内存布局相同,不同定义中的row_major、column_major值必须相等,这个为默认选项;packed表示编译器可以优化统一变量块的内存布局,必须查询偏移位置,而且统一变量块无法在顶点、片段着色器或者程序间共享;std140表示OpenGL ES 3.0规范的标准统一变量块布局;column_major表示矩阵在内存中以列优先顺序布局,这是个默认选项;row_major表示矩阵在内存中以行优先顺序布局。

8、着色器的输入输出

着色器的输入、输出变量分别使用in、out关键字进行修饰,顶点输入变量用于指定顶点着色器中每个顶点的输入,通常存储位置、法线、纹理坐标和颜色这样的数据,和统一变量一样,底层硬件通常在可输入顶点着色器的属性变量数目上有限制,由内建常量gl_MaxVertexAttribs给出,也可以使用glGetIntegerv查询GL_MAX_VERTEX_ATTRIBS得到,OpenGL ES 3.0实现可支持的最小属性为16个。每个顶点着色器将在一个或多个输出变量中输出需要传递给片段着色器的数据,然后,这些变量也会在片段着色器中声明为类型相符的输入变量,在光栅化阶段中对图元进行线性插值,同样,底层硬件通常限制顶点着色器输出、片段着色器输入(硬件上称作插值器)的数量,由内建变量gl_MaxVertexOutputVectors、glMaxFragmentInputVectors给出,或者使用glGetIntegerv查询GL_MAX_VERTEX_OUTPUT_COMPONENTS、GL_MAX_FRAGMENT_INPUT_COMPONENTS得到总分量值数量而非向量数量,OpenGL ES 3.0实现可以支持的最小顶点输出向量数为16,最小片段输入向量数为15。片段着色器输出一个或多个颜色,一般只渲染到一个颜色缓冲区,layout是可选的,但是,当渲染到多个渲染目标(MRT)时,可以使用layout指定每个输出前往的渲染目标,这时在片段着色器中会有一个输出变量,该值将是传递给管线逐片段操作部分的输颜色。注意,与顶点着色器输入不同,顶点着色器输出和片段着色器输入变量不能有布局限定符layout,OpenGL ES实现自动选择位置。下面是顶点、片段着色器的例子。

// vertex shader
#version 300 es
uniform mat4 u_matViewProjection;
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec3 a_color;
out vec3 v_color;
void main()
{
    gl_Position = u_matViewProjection * a_position;
    v_color = a_color;
}
// fragment shader
#version 300 es
precision mediump float;
in vec3 v_color;
layout(location = 0) out vec4 o_fragColor;
void main()
{
    o_fragColor = vec4(v_color, 1.0);
}

9、精度

精度限定符用以指定着色器中任何基于浮点数或者整数的变量的计算精度,包括低、中、高三种精度,关键字分别为lowp、mediump、highp,还有一个关键字precision在着色器的开头用以指定默认精度,精度较低时运行着色器时可能更快,或者电源效率更高,在没有正确使用精度限定符时可能造成伪像。在顶点着色器中,如果没有指定默认精度,则int和float的默认精度都为highp,片段着色器的规则于此不同,浮点值没有默认的精度值,必须进行适当的声明。需要注意的是,精度限定符指定的精度与OpenGL ES特定实现的精度和范围有关。下面是精度限定符的例子。

precision highp float;
precision mediump int;
highp vec4 position;
varying lowp vec4 color;
mediump float specularExp;

10、不变性

着色器需要编译,而编译器可能进行导致指令重新排序的优化,这种指令重排意味着两个着色器之间的等价计算不能保证产生完全相同的结果,这种不一致性在多遍着色器特效时尤其可能成为问题,在这种情况下,相同的对象用alpha混合绘制在自身上方,如果用于计算输出位置的数值的精度不完全一样,精度差异就会导致伪像,这个问题突出表现为深度冲突,每个像素的深度Z精度差异导致不同遍着色相互之间有微小的偏移。为此,引入了invariant不变性关键字,可以用于任何可变的顶点着色器输出,提供了一种途径来规定用于计算输出的相同计算的值必须相同,虽然不变性表示在指定GPU上的计算结果会得到相同的结果,但是并不意味着计算在任何OpenGL ES实现之间保持不变。invariant可以用于变量声明,或者用于已经声明的变量,例子如下。

#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;
}

另外,也可以用如下pragma指令让所有变量全部不变,因为编译器需要保证不变性,所以可能限制它所做的优化,导致性能下降,需要谨慎使用。

#pragma STDGL invariant(all)

11、插值

插值限定符用于顶点着色器的输出和片段着色器的输入,默认为smooth,即平滑着色,来自顶点着色器的输出变量在图元中线性插值,片段着色器接收线性插值之后的数值作为输入,另一种插值方式为平面着色,图元中的值没有进行插值,而是将其中一个顶点视为驱动顶点,该顶点的值被用于图元中的所有片段。插值限定符还有一个关键字centroid,表示质心采样,使用多重采样渲染时,centroid可用于强制插值发生在被渲染图元内部,否则在图元的边缘可能出现伪像。下面是质心采样、平滑着色的着色器输入输出变量。

smooth centroid out vec3 v_color; // vertex shader output
smooth centroid in vec3 v_color; // fragment shader input from vertex shader

12、统一变量和插值器打包

前面提到了统一变量和顶点着色器输出变量与片段着色器输入变量在底层硬件中可用于每个变量存储的资源是固定的,着色器可能声明各种类型的统一变量和着色器输入输出变量,包括标量、向量和矩阵,那么,这些变量如何映射到硬件上的可用物理空间呢?在OpenGL ES 3.0中,这个问题通过打包规则处理,该规则定义统一变量和插值器映射到物理存储空间的方式,打包规则(layout(packed))基于物理存储空间被组织为一个每个存储位置四列和一行的网格的概念,打包规则寻求打包变量,使生成代码的复杂度保持不变,也就是说,打包规则不进行重排序操作,因为重排序操作需要编译器生成合并未打包数据的额外指令,而是试图在不对运行时性能产生负面影响的情况下,优化物理地址空间的使用。需要注意的是,打包影响统一变量和顶点着色器输出与片段着色器输入的计数方式,编写保证能够在所有OpenGL ES 3.0实现上运行的着色器,就不应该使用打包之后超过最小运行存储大小的统一变量和插值器。例如下面的几个统一变量,如果完全不进行打包,m占3行,f占6行,v占1行,共需要10行才能存储这些变量,许多常量存储空间将被浪费,如果使用打包规则,只需使用6个物理常量位置,数组f的元素会跨越行的边界,原因是GPU通常会按照向量位置索引对常量存储进行索引,打包必须使数组跨越行边界,这样索引才能起作用。

uniform mat3 m;
uniform float f[6];
uniform vec3 v;

非打包——

【OpenGL ES】着色语言GLSL_第1张图片

打包——

【OpenGL ES】着色语言GLSL_第2张图片

你可能感兴趣的:(图形图像)