【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

构造体

GLSL ES 支持用户自定义的类型,即 结构体。使用关键字 struct,将已存在的类型聚合到一起,就可以定义为结构体。比如:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

上面这段代码定义了一种新型你的结构体类型 light,它包含两个成员:color 变量和 position 变量。在定义结构体之后,我们又声明了两个 light 变量 l1 和 l2。和C语言不同的是,没有必要使用 typedef 关键字来定义结构体,因为结构体的名称会自动成为类型名。

此外,为了方便,可以在同一条语句中定义结构体并声明该结构体类型的变量,如:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

赋值和构造

结构体有标准的构造函数,其名称与结构体名一直。构造函数的参数的顺序必须与结构体定义中的成员顺序一致。下图显示了结构体构造函数的使用方法。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

访问成员

在结构体变量名后跟点运算符,然后再加上成员们,就可以访问变量的成员。比如:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

运算符

结构体的成员可以参与其自身支持的任何运算,但是结构体本身只支持两种运算:赋值和比较。如下表所示:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

当且仅当两个结构体变量所对应的所有成员都相等时,== 运算符才会返回 ture,如果任意某个成员不相等,那么 != 运算符返回 ture。


数组

GLSL ES 支持数组类型。与JS中的数组不同的是,GLSL ES 只支持一维数组,而且数组对象不支持 pop()和 push()等操作,创建数组时不需要使用 new 运算符。声明数组很简单,只需要在变量名后加上中括号和数组的长度。比如:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

数组的长度必须是大于0的整型常量表达式,如下定义:

  • 整形字面量(如 0 或 1)。
  • 用 const 限定字修饰的全局变量或局部变量,不包括函数参数。
  • 由前述两条中的项组成的表达式。

因此,下面的代码将会出错:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

注意,你不可以用 const 限定字来修饰数组本身。

数组元素可以通过索引值来访问,和C语言一样,索引值也是从0开始的。比如,下面这句代码就可以访问 floatArray 变量的第3个元素:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

只有整型常量表达式和 unoform 变量可以用作数组的索引值。此外,与JS或C不同,数组不能在声明时被一次性地初始化,而必须显示地对每个元素进行初始化。如下所示:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

数组本身只支持 [] 运算符,但数组的额元素能够参与其自身类型支持的任意运算。比如,floatArray 和 vec4Array 的元素可以参与下面这些运算:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)


取样器(纹理)

将GLSL ES 支持的一种内置类型称为取样器,我们必须通过该类型变量访问纹理。有两种基本的取样器类型:sampler2D 和 samplerCube。取样器变量只能是 uniform 变量,或者需要访问纹理的函数,如texture2D()函数的参数。比如:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

此外,唯一能赋值给取样器变量的就是纹理单元编号,而且你必须使用 WebGL 方法 gl.uniformli()来进行赋值。

除了=、==和!=,取样器变量不可以作为操作数参与运算。

和前几节中介绍的额基本类型不同,取样器类型变量收到着色器支持的纹理单元最大数量限制,如下表所示。该表格中 mediump 是一个精度限定字。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)


运算符优先级

运算符优先顺序如下表所示:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)


程序流程控制:分支和循环

着色器中的分支与循环与JS 或 C 中的几乎无异。

if 语句和 if-else 语句

可以使用 if 语句或 if-else 语句进行分支判断,以控制程序流程。下面是使用 if-else 语句的格式:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

下面是一段使用 if-else 语句的代码示例:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

如例中所示,if 语句或 if-else 语句中都必须包含一个布尔值,或者是产生布尔值的表达式。此处不可以使用布尔值类型矢量,比如 bvec2。

GLSL ES 中没有 switch 语句,你也应该注意,过多的 if 或 if-else 语句会降低着色器的执行速度。

for 语句

for 语句的格式如下所示:
【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

  • 列表内容

比如:
【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

注意,循环变量只能在初始化表达式中定义,条件表达式可以为空,如果这样做,空的条件表达式返回true。此外,for 语句还有这样一些限制。

  • 只允许有一个循环变量,循环变量只能是 int 或 float 类型。
  • 循环表达式必须是以下的形式:
    -i++, i– i+= 常量表达式或 i-= 常量表达式;
  • 条件表达式必须是循环变量与整形常量的比较。
  • 在循环体内,循环变量不可被赋值。

这些限制的存在是为了使编译器就能够对for循环进行内联展开。

continue、break 和 discard语句

就像在JS和C语言一样,我们只能在 for 语句中使用 continue 和 break,通常,我们将它们与 if语句搭配使用。

  • continue 中止包含该语句的最内层循环和执行循环表达式然后执行下一次循环。
  • break 中止包含该语句的最内层循环,并不再继续执行循环。

下面是使用 continue 的示例:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

下面是使用 break 的示例:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

关于 discard,它只能在片元着色器中使用,表示放弃当前片元直接处理下一个片元,第10章会详述。


函数

与JS中函数的定义的方式不同,GLSL ES 定义函数的方式更接近于C语言,其格式如下:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

参数的 type 必须为本章所述的类型之一,或者像main()函数这样没有参数也是允许的。如果函数不返回至,那么函数中就不需要有 return 语句。返回类型必须是 void。你也可以将自己定义的结构体类型指定为返回类型,但是构造体的成员中不能有数组。

下面这段代码是一个函数,实现将 RGBA 颜色值转化为亮度值。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

声明了函数后,你就可以调用它了,其方法与 JS 和 C 语言中的相同,通过函数名和参数序列来调用:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

注意,如果调用函数时传入的参数类型与声明函数时指定的参数类型不一致,就会出错。比如,下面这段代码就会出错,因为函数声明时的参数是 float 类型,而调用时却传入了 int 类型的值。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

如你所见,函数正如你语气的那样运行了。但是,和 C 与 JS 不同的是,你不能在一个函数内部调用它本身。这项限制的目的也是为了便于编译器对函数进行内联展开。

规范声明

如果函数定义在调用之后,那么我们必须在进行调用之前先声明该函数的规范。规范声明会预先告诉 WebGL 系统函数的参数、参数类型、返回值等等。这一点与 JS 截然不同,后者不需要提前声明函数。下面这段代码对前一节示例中的 luma()函数提前做了规范声明:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

参数限定词

在 GLSL ES 中,可以作为函数参数指定限定字,以控制参数的行为。我么您可以将函数参数定义成:

  1. 传递给函数的
  2. 将在函数中被赋值的
  3. 既是传递给函数的,也是将要在函数中被赋值的。

其中 第2点和第3点都类似于C语言的指针。下表显示了这些参数的限定字。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

比如说,我们可以给 luma()函数指定一个 out 参数,让其接受函数的计算结果。而在此之前,我们是通过返回值来向外部反馈计算结果的。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

修改之后,函数本身不返回值,所以函数的返回类型设为 void。此外,我们还在第1个参数之前添加了限定词 In,实际上这可以省略,因为 in 是默认的限定字。

然后,我们可以这样调用函数:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)


内置函数

除了允许用户自定义函数,GLSL ES 还提供了很多常用的内置函数。下表概括了 GLSL ES 的内置函数。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)


全局变量和局部变量

就像 JS 和 C 语言,GLSL ES 中也有全局变量和局部变量的概念。全局变量可以在程序中的任意位置使用,而局部变量只能在有限的某一部分代码中使用。

在GLSL ES 中,如果变量声明在函数的外面,那么它就是全局变量,如果声明在函数内部,那就是局部变量。这和 JS 与 C 语言也是一样的。局部变量只能在函数内部使用,因此,由于需要在函数外部访问,下一节中将涉及的 attribute 变量、varying 变量和 uniform 变量都必须声明为全局变量。


存储限定字

在GLSL ES 中,我们经常使用 attribute、varying 金额 uniform 限定字来修饰变量,如下图所示。才外,我们有时也会使用 const 限定字,它表示着色器中的某个变量是恒定的常量。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

const 变量

JS中没有 const 变量的概念,但是 GLSL ES 中有。我们使用 const 限定字表示该变量的值不能被改变。

在声明 const 变量时,需要将 const 卸载类型之前,就像声明 attribute 变量时将 attribute 鞋子啊前面一样。声明同时必须对它进行初始化,声明之后就不能再去改变它们的值了。比如:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

试图向 const 变量赋值会导致编译报错,比如:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

将会产生如下错误信息:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

Attribute 变量

你一定已经很熟悉 attribute 变量了。attribute 变量只能出现在顶点着色器中,只能被声明为全局变量,被用来表示逐顶点的信息。你应该重点理解”逐顶点“的含义。比如,如果线段有两个顶点(4.0, 3.0, 6.0)和(8.0, 3.0, 0.0),这两个坐标就会传递给 attribute 变量。而线段上的其他点,比如中点(6.0, 3.0, 3.0),虽然也被画了出来,但它不是顶点,坐标未层传递给 attribute 变量,也未层被顶点着色器处理过。如果你想要让顶点着色器处理它,你将需要将它作为一个顶点添加到图形钟来。attribute 变量的类型只能是 float、vec2、vec3、vec4、mat2、mat3 和 mat4。比如:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

顶点着色器中能够容纳的 attribute 变量的最大数目与设备有关,你可以通过访问内置的全局常量来获取该值。但是,不管设备配置如何,支持 WebGL 的环境都支持至少8个 attribute 变量,如下表所示:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

uniform 变量

uniform 变量可以用在顶点着色器和片元着色器中,且必须是全局变量。uniform 变量是只读的,它可以是除了数组或结构体之外的任意类型。如果顶点着色器和片元着色器中声明了同名的 uniform 变量,那么它就会被两种着色器共享。uniform 变量包含了”一致“(非逐顶点/逐片元的,各顶点或各片元公用)的数据,JS 应该向其传递此类数据。比如,变换矩阵就不是逐顶点的,而是所有顶点公用的,所以它在着色器中是 uniform 变量。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

varying 变量

最后一个限定字是 varying。varying 变量必须是全局变量,它的任务是从顶点着色器向偏远着色器传输数据。我们必须在两种着色器中声明同名、同类型的 varing 变量:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

和 attribute 变量一样。varying 变量只能是以下类型:float、vec2、vec3、vec4、mat2、mat3 和 mat4。如第5章所述,顶点着色器赋给 varying 变量的值并不是直接传给了片元着色器的 varying变量,这其中发生了光栅化的过程:根据绘制的图形,对前者进行内插,然后再传递给后者。正式因为 varying变量需要被内插,所以我们需要限定它的数据类型。


精度限定字

GLSL ES 新引入了精度限定字,目的是帮助着色器程序提高运行效率,削减内存开支。顾名思义,精度限定字用来表示美中数据具有的精度。简而言之,高精度的程序需要更大的开销,而低精度的程序需要的开销则小得多。使用精度限定字,你就能够精细地控制在效果和性能间的平衡。然后,精度限定字是可选的,如果你不确定,可以使用下面这个适中的默认值:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

由于WebGL 是基于 OpenGL ES 2.0 的,WebGL 程序最后有可能运行在各样的硬件平台上。肯定存在某些情况需要在低精度下运行程序,以提高内存使用效率,减小性能开销,以及更重要的,降低能耗,延长移动设备的电池续航能力。

注意,在低精度下,WebGL 程序的结果会比较粗糙或不正确,你必须在程序效果和性能间进行平衡。

如下表所示,WebGL 程序支持三种精度,其限定字分别为 highp(高精度)、mediump(中精度)和 lowp(低精度)。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

还有两点值得注意。首先,在某些 WebGL 环境中,片元着色器可能不支持 highp 精度,检查的方法稍后再讨论;其次,数值范围和精度实际上也是与系统环境相关的,你可以使用 gl.getShaderPrecisionFormat()检查。

下面是声明变量精度的几个例子:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

为每个变量都声明精度很繁琐,我们也可以使用关键字 precision 来声明着色器的默认精度,这行代码必须在顶点着色器或片元着色器的顶部,其格式如下:
【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

这句代码表示,在哪着色器中,某种类型的变量其默认精度由精度限定字指定。也就是说,接下来所有不以精度限定字修饰的该类型变量,其精度就是默认精度。比如:
【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)
【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

上面这段代码表示,所有 float 类型以及相关的 vec2 和 mat3 的变量都是中精度的,所有整型变量都是高精度的。比如 vec4 类型变量的四个分量都是中精度的。

你也许已经注意到,在前几章我们并没有限定类型的精度。这是因为,对于这些类型,着色器已经实现了默认的精度,只有片元着色器中的 float 类型没有默认精度。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

事实就是,片元着色器中的 float 类型没有默认精度,我们需要手动指定。如果我们不再偏远着色器中限定 float 类型的精度,就会导致如下的编译错误:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

我们说过,WebGL 是否在片元着色器中支持highp 精度,取决于具体的设备。


预处理指令

GLSL ES 支持预处理指令。预处理指令用来在真正编译之前对代码进行预处理,都以井号开始。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

这段代码检查了是否已经定义了 GL_ES 宏,如果是,那就执行 #ifdef 和 #endif 之间的部分。这个预处理指令的格式和 C 语言或 JS 中的 if 语句很类似。

下面是我们在 GLSL ES 中可能用到的三种预处理指令。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

你可以使用 #define 指令进行宏定义。和 C 语言中的宏不同,GLSL ES 中的宏没有宏参数:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

你可以使用 #undef 指令接触宏定义:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

你可以使用 #else 指令配合 #ifdef (就像C语言或JS中 if 语句中的 else),比如:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

宏的名称可以任意起,只要不和预定义的内置宏名称相同即可。

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

所以我们可以这样使用宏来进行精度限定:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)
【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

可以使用#version 来指定着色器使用的 GLSL ES 版本:

【笔记】《WebGL编程指南》学习-第6章着色器语言(3-其他)

可以接受的版本包括 100(GLSL ES 1.00)和101(GLSL ES 1.01)。如果不使用 #version 指令,着色器将默认的版本是1.00。