OpenGL ES 3.0(二)着色器语言

OpenGL ES 着色器语言:OpenGL ES Shading Language,下面简写为 ES SL 或 SL。

着色器版本指定

#version 300 es

如果没有指定,则默认为 1.00,这是 OpenGL ES 2.0 使用的版本,在 OpenGL ES 3.0 中,制定规范的作者决定匹配 OpenGL ES 和 ES SL 的版本,因此直接从 1.0 跳到了 3.0。

变量类型

标量 float, int, uint, bool 基于标量的数据类型
浮点向量 float, vec2, vec3, vec4 分别表示有 1、2、3、4 个成员的浮点向量
整型向量 int, ivec2, ivec3, ivec4 分别表示有 1、2、3、4 个成员的整型向量
无符号整型向量 uint, uvec2, uvec3, uvec4 分别表示有 1、2、3、4 个成员的无符号整型向量
布尔向量 bool, bvec2, bvec3, bvec4 分别表示有 1、2、3、4 个成员的布尔向量
矩阵 mat2 (or mat2x2), mat2x3, mat2x4, mat3x2, mat3 (or mat3x3), mat3x4, mat4x2, mat4x3, mat4 (or mat4x4) 分别表示 2x2、2x3、...、4x4 矩阵

构造函数

在 OpenGL ES SL 中,对于类型转换有很严格的限制,即变量只允许使用相同类型的其它变量进行赋值或其它操作。但 SL 中提供了一些构造函数可以用于类型转换:

float myFloat = 1.0;
float myFloat2 = 1; // 错误,不正确的类型转换
bool myBool = true;
int myInt = 0;
int myInt2 = 0.0; // 错误,不正切的类型转换
myFloat = float(myBool); // 正确,bool -> float
myFloat = 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}

矩阵类型的构造函数则更加灵活,规则如下:
1) 如果只提供了一个标量(基本数据类型),则会用这个值填充矩阵的对角线,比如 mat4(1.0) 将会构造一个 4x4 的单位矩阵
2) 可以使用多个向量构造,比如 mat2 可以使用两个 vec2 构造
3) 可以使用多个标量构造

需要注意的是,和平时的写法不同,OpenGL 中的矩阵以列优先的形式排序:

mat3 myMat3 = mat3(1.0, 0.0, 0.0,  // 第一列
                   0.0, 1.0, 0.0,  // 第二列
                   0.0, 1.0, 1.0); // 第三列

向量和矩阵的成员

根据组成 vector 的成员数量,可以通过 {x, y, z, w}, {r, g, b, a}, 或 {s, t, p, q} 来访问它的成员,x、r、s 永远指向第一个成员,使用不同的名字仅仅只是为了方便,但不能混合使用,即不能用 ".xgr" 这样的形式。使用示例:

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}

也可以使用 "[]" 操作符,记住,矩阵是以列排序的:

mat4 myMat4 = mat4(1.0);     // 初始化对角线的值为 1.0 (单位矩阵)
vec4 col0 = myMat4[0];          // 从矩阵中获取第 0 列向量
float m1_1 = myMat4[1][1];    // 从矩阵中获取元素 [1][1]
float m2_2 = myMat4[2].z;      // 从矩阵中获取元素 [2][2]

常量

常量必须在声明的时候初始化:

const float zero = 0.0;
const float pi = 3.14159;
const vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
const mat4 identity = mat4(1.0);

Structures

结构体定义:

struct fogStruct
{
    vec4 color;
    float start;
    float end;
} fogVar;

结构体被定义的同时,它的构造函数也被定义:

fogVar = fogStruct(vec4(0.0, 1.0, 0.0, 0.0), // color
                               0.5,     // start
                               2.0);    // end

注意形参和实参必须一一对应。

访问结构体成员:

vec4 color = fogVar.color;
float start = fogVar.start;
float end = fogVar.end;

数组

数组声明及初始化:

float floatArray[4];
vec4 vecArray[2];

float a[4] = float[](1.0, 2.0, 3.0, 4.0);
float b[4] = float[4](1.0, 2.0, 3.0, 4.0);
vec2 c[2] = vec2[2](vec2(1.0), vec2(1.0));

函数

OpenGL ES SL 中定义函数的方法和 C 差不多,最重要的一点区别是,OpenGL ES SL 中提供了几个特殊的标识符,表明某一个参数是否会在函数内部被更改:

Qualifier Description
in 如果没有指定,则默认为 in 表明变量以传值的形式进入到函数中,不会被更改
inout 表明变量以引用的形式传递到函数中,可能会被更改
out 表明变量的值不会被传递到函数中,但会在函数返回值被更改

例如:

vec4 myFunc(inout float myFloat,  // inout 参数
            out vec4 myVec4,     // out 参数
            mat4 myMat4);         // in 参数(默认)

值得注意的是,在 OpenGL ES SL 中,函数不能递归,这是因为有一些 GPU 调用函数的方式为 inline。

内置函数

OpenGL ES 中定义了许多内置函数可供开发者直接使用,比如下面这个计算漫射反光的函数:

vec4 diffuse(vec3 normal, vec3 light, vec4 baseColor) {
    return baseColor * dot(normal, light);
}

这个函数调用了一个内置函数 dot,dot 用于计算点积,即矩阵各个对应元素相乘,要求两个矩阵具有相同的大小。

下面是镜面反射(specular lighting )的计算过程:

float nDotL = dot(normal, light);
float rDotV = dot(viewDir, (2.0 * normal) * nDotL - light);
float specular = specularColor * pow(rDotV, specularPower);

流程控制语句

使用方法和 C 一样,但在 OpenGL ES 2.0 中,对循环的使用有着非常严格的规则控制,只支持编译器可以展开的循环;而在 OpenGL 3.0 中,这些限制不再存在。但循环的使用依然会对性能有一些影响,因此,应该尽量限制流程控制语句的使用。

Uniforms

和 attribute(2.0) 或 in(3.0) 数据一样,Uniforms 也是由外部程序输入到着色器代码中的,所不同的是,它被同一 program 中的顶点、片段着色器共享,且着色器不能更改。Uniform 变量存储在硬件上的“常量存储区”中,因为这个区域的尺寸是固定的,因此同一个 program 中的 uniform 数量是有限制的。可以使用 gl_MaxVertexUniformVectors 和 gl_MaxFragmentUniformVectors 确认可使用的最大 uniform 个数,或使用函数 glGetintegerv(参数为 GL_MAX_VERTEX_UNIFORM_VECTORS 或 GL_MAX_FRAGMENT_UNIFORM_VECTORS) 获取。

OpenGL ES 3.0 保证最少有 256 个 vertex uniform vector,224 个 fragment uniform vector 可用。

Uniform Blocks

使用 uniform buffer object 通常有几个优点:
1) uniform 数据可以在多个 program 之间共享(注意仅能设置一次)
2) 允许存储数量更多的 uniform
3) 在 uniform buffer object 之间切换可能比一次单独地加载一个 uniform 效率更高

uniform buffer object 通过 uniform block 的形式使用:

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

in、out

OpenGL ES SL 中另一个特殊类型是 vertex input (or attribute) 变量, 用 in 修饰,用于指定顶点着色器中每一个顶点的输入,通常用于存储位置、纹理坐标、颜色等信息。代码示例:

#version 300 es
layout(location=0) in vec4 aColor;
layout(location=1) in vec4 aPosition;
out vec4 vColor;
void main() {
    vColor = aColor;
    gl_Position = aPosition;
}

layout 限定符相当于 index,用于分辨顶点属性,如果没有指定,链接器会自动为其赋值。

和 uniform 变量一样,硬件对 attribute 变量的数量有限制,可以通过 gl_MaxVertexAttribs 或 glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) 查询,OpenGL ES 3.0 保证最少能支持 16 个,因此如果希望编写在任何 OpenGL ES 3.0 的实现上都能运行的着色器,应该限制不超过 16 个 attribute。

顶点着色器的输出变量用 out 修饰,对应于片元着色器中使用 in 修饰对应的变量。这些数据会被输出到片元着色器中,并且在光栅化过程中,会对图元进行线性插值。注意不能使用 layout 限定符修饰,程序实现会自动选择它们的 location 值。同样的,顶点着色器的输出变量在硬件中也有数量限制,可以通过 gl_MaxVertexOutputVectors 或 glGetIntegerv(GL_MAX_VERTEX_OUTPUT_COMPONENTS,返回总的成员数量,而不是 vector 的数量) 查询,OpenGL ES 3.0 最少能支持 16 个。而片元着色器 in 变量的数量限制可以通过 gl_MaxFragmentInputVectors 或 glGetIntegerv (GL_MAX_FRAGMENT_INPUT_COMPONENTS,返回总的成员数量,而不是 vector 的数量) 获取,OpenGL ES 3.0 最少能支持 15 个。

通常,片元着色器仅需要渲染到一个颜色缓冲区中,但如果存在多个渲染目标,则需要使用 layout 限定符指定,比如:

#version 300 es
precision mediump float;
layout(location = 0) out vec4 fragData0;
layout(location = 1) out vec4 fragData1;
layout(location = 2) out vec4 fragData2;
layout(location = 3) out vec4 fragData3;
void main()
{
    fragData0 = vec4 ( 1, 0, 0, 1 );
    fragData1 = vec4 ( 0, 1, 0, 1 );
    fragData2 = vec4 ( 0, 0, 1, 1 );
    fragData3 = vec4 ( 1, 1, 0, 1 );
}

插值限定符

顶点着色器、片元着色器的输入变量可以指定插值限定符,如果没有指定,则默认使用 smooth 差值方法渲染,也就是说,图元会在顶点着色器的输出变量的值的基础上进行线性插值,片元着色器接收到的是经过线性插值后的结果。可以在着色器代码中显式地指定插值方式:

// 顶点着色器
smooth out vec3 v_color;

// 片段着色器
smooth in vec3 v_color;

另一个插值方式为 flat shading,这种方式不会在图元上插值,而是使用一个激发顶点(provoking vertex),该顶点的值会被应用于图元中的所有片段:

// 顶点着色器
flat out vec3 v_color;

// 片段着色器
flat in vec3 v_color;

最后一个可以加入到插值器中的是 centroid 关键字,可以强制插值作用于图元的内部,否则,在图元边缘可能会出现伪像。使用:

// 顶点着色器
smooth centroid out vec3 v_color;

// 片段着色器
smooth centroid in vec3 v_color;

预处理器

和 C 类似,有:

#define
#undef
#if
#ifdef
#ifndef
#else
#elif
#endif

以下是一些内置的宏:

__LINE__            // 替换为着色中的当前行号
__VERSION__    // OpenGL ES 着色器语言版本 (例如 300)

另外有:

#error // 将在着色器编译期间导致编译错误,并在信息日志中放置相应的消息
#pragma // 用于改为编译器指定 implementation-specificc 指令

还有一个重要的指令是 #extension,用于启用和设置扩展的行为:

// 设置某个扩展的行为
#extension extension_name : behavior
// 设置所有扩展的行为
#extension all : behavior

behavior 的可选值有:require、enable、warn、disable。

精度限定符

精度限定符可以指定着色器变量进行计算时的精度,可选值有:lowp、mediump、highp。低精度在一些 OpenGL ES 的实现中可能会带有速度及效率的提升,但精度的降低可能造成伪像(artifacts)现象。但 OpenGL ES 规范中没有规定必须支持多个精度,因此所有计算以最高精度执行并忽略精度限定符是有效的。

精度限定符可用于修饰 flocating-point 和 integer-based 变量,示例:

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

可以在着色器顶部指定默认精度:

precision highp float;
precision mediump int;

如果没有指定,则 int 和 flocat 都使用 highp 作为默认精度。但在片元着色器中,必须指定一个默认的 flocat 精度。

你可能感兴趣的:(OpenGL ES 3.0(二)着色器语言)