出处:电子设备中的画家|王烁 于 2017 年 7 月 11 日发表,原文链接(http://geekfaner.com/shineengine/blog6_OpenGLESv2_5.html)
上节回顾
上一节了解了 GLSL 中的变量类型,有一些类型是原本所熟知的,而有些类型是 GLSL 特有的。在上一节结尾的时候也提到了变量的范围,其中讲到两个 shader 所在的空间是独立的,但是由于 shader 本身的功能,需要在不同的 shader 之间传递数据,也需要与 OpenGL ES 进行交互,那么就需要有办法进行跨越空间的交流。在 C 语言中,是通过 static 和 extern 这两个变量修饰符,在 C++中,是通过 public。而在 Shader 中也存在类似的修饰符,并且 shader 中的修饰符种类很多,且非常重要。那么这一节,我们详细讲解的 GLSL 变量的修饰符。
GLSL 的变量修饰符
GLSL 中,变量的定义除了上一节介绍的知识点外,还有一个重要的知识点, 就是每个变量都有修饰符,且修饰符也分为很多种。
在定义变量的时候,除了要指明必须的变量类型和变量名,我们还可以在变量类型之前加修饰符。在 C、C++中我们也都见过修饰符,比如 const 就属于修饰符,假如我要定义一个 const 的 float 类型变量,可以写是 const float x = 1.5, 那么这个变量 x 就成了一个只读的变量,x 也就成了一个常量 1.5。其中 float 是变量类型,x 是变量,=1.5 是对变量的初始化,而 const 就是修饰符。
在 C 或者 C++中,我们可能会修饰符用的很少,而在 GLSL 中,存在着大量的修饰符,而且这些修饰符会被广泛使用着。
这一节,将重点讲解变量的这些修饰符。
存储修饰符
修饰符也分种类,就好像形容词一样,一个变量只能有一个变量类型,但是它可以有很多形容词。首先说的修饰符,叫做存储修饰符。存储修饰符,顾名思义就是用于区分不同变量的存储空间的。
我们知道 shader 分为 Vertex shader 和 Fragment shader。那么可以把它们看作是两个独立的空间,而 OpenGL ES 又可以看作是第三个独立空间。但是这三个空间又存在大量的联系,比如 OpenGL ES 需要向 VS 和 PS 中传输数据,VS 也需要向 PS 传输数据。那么使用什么变量来进行数据传输,非常重要。那么说到这里,大家就可以猜到,其实是通过存储修饰符来区分不同的变量,让这些变量来进行不同空间的数据传输的。
< none: default >
存储修饰符分 5 种,第一种是没有存储修饰符,那么这种变量也就被称为 local 变量,只能在一个空间中使用的。
然而在一个空间中也存在全局变量。也就是在 main 函数以及其他函数之外定义一个变量,这个变量可以被初始化,但是只能初始化成一个常量表达式。而如果全局变量不被初始化,那么它不会有默认值,处理的时候也会按照 undefine 进行处理。
无论是全局变量还是普通的 local 变量,只要没有被存储修饰符,就会通过它所在空间的那个 shader 进行内存分配。而对应的变量名也就是用于访问那一块内存使用。
const
其实也就是我们熟悉的 const,这种变量可以被认为是编译时的常量。使用这种常量比直接使用数字常量看起来更加方便一些。提高代码的可读性。
const 修饰的变量也是 local 变量,且在当前 shader 中属于只读变量。const 变量只能在定义的时候进行初始化,如果在定义之后再进行赋值,就会出错。
struct 的结构体的成员,不可以被 const 修饰,但是使用自定义 struct 变量类型创建的变量,可以被 const 修饰,然后通过 struct 的构造函数进行初始化。
对 const 变量进行初始化的时候也必须使用常量表达式。
然而 array,或者包含 array 的 struct,由于 array 不能被初始化,所以它们不能被 const 修饰。
const 还有另外一种用法,就是作为函数的参数,表明传入的参数是只读的。
函数参数也只能使用 const 这一种存储修饰符。而函数返回值则不适用存储修饰符。
attribute
用处是从 OpenGL ES 中向 VS 中传输数据的时候使用,OpenGL ES 可以通过 API 得到指定 VS 中某个 attribute 的位置,然后通过这个位置以及另外一个 API, 将数据传输到 VS 中。主要是用于传入顶点坐标、颜色等信息的。
attribute 变量只能被定义在 VS 中,如果在别的 shader 中定义 attribute 变量会出现错误。
attribute 变量在 shader 中属于只读变量。
attribute 修饰符只能修饰 float、vec2、vec3、vec4、mat2、mat3、mat4 类型的变量,可以看出这些全部都是只包含 float 类型变量的变量。
在说 vec 类型的变量的时候,我们就说了,GPU 支持 vec 类型变量的目的是为了减少运算次数,达到一次运算得到之前多次运算结果的目的。而且 GPU 硬件为了配合支持这种运算,也支持 vec 这种类型的格式。而在这里,attribute 也支持 vec 类型的变量,也就是可以将 CPU 传输过来的数据,保存到 GPU 中支持这种格式的硬件中。但是 GPU 中的这种硬件也是有限的,也就是说只能保存一定数量的 vec 类型变量。而这种类型的变量又非常好用,所以需要尽可能多的使用这种类型的变量,于是,硬件层面上,将 attribute 所能存放的区域全部都使用的是这种类型的硬件,所以如果 attribute 修饰的变量为 float,也会占用一个 vec4 的位置,所以 attribute 应该尽可能的组合成 vec4 类型的变量。而 mat4 则占用了 4 个 vec4 的位置,mat2 占用了 2 个,mat3 占用了 3 个。虽然 GPU 中,attribute 所能存放的区域都使用这种类型的硬件,但是依然是有限的,所以硬件支持的在一个 shader 中使用的 attribute 的数量是有限的,这个限制在 Khronos(OpenGL ES Spec 的制定者)那里有最低限制,但是每个平台不同,支持的 attribute 数量也就不同。
attribute 修饰符不能修饰 array 或者 struct 类型的变量。原因也就比较简单了,因为 array 和 struct 不容易控制他们的尺寸和数量。
这里需要注意的一点是,如果一个 attribute 在 shader 中被定义了,但是没有被使用,则在计算 shader 中 attribute 数量的时候,它不会被计算上,因为它会被 shader 优化掉。
另外,shader 中所有的 attribute,都必须是全局变量,也就是 attribute 的定义必须在 main 函数以及其他函数之外。
uniform
用处是从 OpenGL ES 中同时向 VS 和 PS 传输数据的时候使用。OpenGL ES 可以通过 API 得到 shader 中某个 uniform 的位置,然后通过这个位置以及另外一个 API,将数据传输到 shader 中的该 uniform。在 shader 中,uniform 属于只读数据。 uniform 修饰符修饰的变量,可以是任何类型,甚至是 array 和 struct。
和 attribute 类型,uniform 也受到了限制,不同的是,attribute 是数量受到了限制,而 uniform 是尺寸受到了限制,因为毕竟 uniform 支持 array 和 struct, 只限制数量意义不大。至于这个限制也在 Khronos(OpenGL ES Spec 的制定者) 那里有最低限制,但是每个平台不同,支持的 uniform 的尺寸也就不同。同样的, 如果一个 uniform 被定义了,但是没有被使用,则它也不会被加入用于计算 uniform 的尺寸,因为它也会被优化掉。
但是还有一点需要注意,除了开发者定义的 uniform,shader 本身还会有 build-in 的 unform,这些 uniform 也会被加入用于计算 uniform 的尺寸,用于判断是否超过限制,build-in 的 uniform 我们会在后面进行解释说明。
如果超过限制了的话,会导致 shader 编译时错误或者链接时的错误。
uniform 被称为 global 变量,区别于 local 变量以及 attribute 中的全局变量, uniform 不止是在当前 shader 必须是全局变量,如果一个 VS 和一个 PS 被链接在一起使用,那么它们会使用同一个 global uniform name space,也就是说,如果 VS 和 PS 中分别定义了一个变量名相同的 uniform,那么它们的变量类型和精度修饰符等信息必须完全一样,然后可以认为这两个变量是一个变量。
uniform 主要用于传入矩阵、纹理贴图等信息的。
varying
用处是从 VS 向 PS 传输数据的时候使用。在说 OpenGL ES pipeline 的时候我们介绍过,VS 运算得到的结果是 OpenGL ES 传入的几个图形关键点的最终坐标, 然后从 VS 到 PS 的时候会经过光珊化,光珊化会根据这些关键点生成很多点用来组成图形的形状。那么假如在 VS 中定义一个 varying 变量,那么 VS 中运算的每一个点都会包含一个 varying 变量对应的值,而 VS 只会针对 OpenGL ES 制定的几个点做运算,假如 OpenGL ES 只是要画一个三角形,那么经过 VS,就会得到这个三角形三个点的顶点坐标值。而如果将三个点的颜色通过 attribute 传入 VS, 那么 VS 就知道这三个点的颜色值,然后再通过 varying 把这三个点颜色传入 PS, 那么在传递的过程中,光珊化的时候,会根据原本那三个顶点包含的颜色值,进行插值,赋值给产生的新点。然后在 PS 中,会对所有产生的点进行运算,而针对每个点进行运算的时候,每个点都会有一个 varying 值,而这个 varying 值都是经过光珊化产生的新值。
光珊化受到 single-sample 和 multi-sample 的影响,也就是插值的算法不同, 等以后讲 OpenGL ES 算法的时候再进行详细讲解,这里就不进行展开说明了。
varying 在 VS 中是可读可写的,但是如果在其还没有被写之前,就对其进行读取,那么读到的将会是 undefine。
varying 在 PS 中是可读不可写的,读取的就是 PS 当前处理像素点经过光珊化后生成的 varying 值。
类似于 uniform,如果一个 VS 和一个 PS 链接在了一起,那么如果在 VS 和 PS 中分别定义了一个变量名相同的 varying 变量,那么它们的类型必须相同,否则链接就会出错。这两个变量的精度修饰符可以不同。
如果在 PS 中没有定义 varying,或者定义了没有使用。那么在 VS 中,无论是没有定义 varying,还是定义了没有使用,或者是定义了且使用了,那么都没有问题。
如果在 PS 中定义了并且使用了一个 varying,但是在 VS 中没有定义,则会出错;而如果在 VS 中定义了没有使用,没有问题,但是 PS 中读取到的这个 varying 的值则是 undefine;而如果在 VS 中定义了且使用了,如果使用的方式是给该 varying 赋值,那么没有问题,如果使用的方式没有给 varying 赋值,也没有问题, 只是 PS 读取到的这个 varying 的值还是 undefine。
解释一下这里所指的使用,只是单纯的说在 shader 中有一个语句中出现了这个 varying,不管是对这个 varying 进行赋值还是读取,甚至该语句没有被真正执行到,都算是对这个 varying 进行了使用。
varying 修饰符修饰的变量,可以是 float、vec2、vec3、vec4、mat2、mat3、mat4 或者是它们对应的 array 类型。但是不可以是 struct。
varying 变量也必须是全局变量,也就是 varying 的定义必须在 main 函数以及其他函数之外。
所以,我们看到了通过存储修饰符,可以实现 OpenGL ES、VS、PS 之间的数据传输,但是我们延伸思考一下,由于 VS 都会对多个顶点进行运算,那么在 VS 的多次运算之间,是否可以实现数值传输呢,也就是将上一个顶点的运算结果传 给下一个顶点运算使用。那么我来借用刚才那个画三角形的例子解释一下,在那个例子中 VS 做了三次运算,PS 做了更多次的运算。但是在 VS 的三次运算之间是不能做数据传输的,PS 的也同样,不能将上一个顶点运算的结果传给下一个顶点使用。这是由于在 GPU 中,一般都是有很多核的,可能会同时进行多个顶点的运算,比如 VS 中可能三个顶点的运算是同时运行的,如果上一个顶点运算的结果可以传给下一个顶点运算时使用,会导致不能并行进行顶点运算,这样会导致 GPU 失去其相对于 CPU 的优势。
上个课时我们提到了共享全局,在 GLSL 中,唯一的共享全局就是 uniform。 varying 不被认为是共享全局的,因为它必须在 VS 和 PS 中同时定义,并且通过 VS 传给光珊化,再传给 PS。共享全局的变量必须有相同的名字、类型、存储、精度修饰符。
参数修饰符
下面要说的修饰符,叫做参数修饰符。
参数修饰符是用于函数的参数列表中。在别的有些语言中,也出现过类似的修饰符。参数修饰符分为四种。
< none: default > in
一般默认,函数的传入参数是将一个变量传入函数中。所以如果函数的传入参数中没有参数修饰符,就等同于使用了 in 这个参数修饰符。
在函数中,如果一个参数是使用 in 修饰,而且没有被 const 修饰,那么它在函数中也可以被修改,但是其实修改的是函数中的那个变量的副本,并非传入的那个函数,所以在函数之外,这个参数的值是不变的。
out
函数的参数在传入的时候没有被初始化,然后应该在函数中进行赋值,然后在函数外,也就是在调用函数的代码块中被使用。
out 变量不能被 const 修饰,如果函数中没有对 out 变量进行赋值,那么 out 变量为 undefine。
Inout
函数的参数被传入,然后在函数中再次被进行更新赋值,然后在函数外,也会被使用。inout 变量不能被 const 修饰。
这里先说一下这些参数的大概意义,具体的等讲到 GLSL 函数语法的时候再 进行详细解释说明。
精度修饰符
第三种修饰符,精度修饰符。其实在说存储修饰符的时候,我们就稍微提到了精度修饰符。当时我们说,针对 uniform,如果把一个 VS 和 PS 链接在一起, 且两个 shader 中分别定义了变量名相同的 uniform,那么它们的精度修饰符也必须相同;而针对 varying,如果把一个 VS 和 PS 链接在一起,且两个 shader 中分 别定义了变量名相同的 varying,那么它们的精度修饰符可以不相同。
精度修饰符是什么,顾名思义,精度修饰符就是用于决定所定义的变量最小可支持的范围和精度。
先来解释一下什么是最小可支持的范围和精度。在 shader 中,对 VS 和 PS 中的 float 和 int 都是有最小可支持的范围和精度的。
比如在 VS 中,float 类型的变量支持的范围至少为负的 2 的 62 次方,到 2 的 62 次方。精度最少是 65536 分之 1。而 VS 中的 int 的范围,至少是负的 2 的 16 次方,到 2 的 16 次方。
当然在 PS 中,希望 float 类型支持的变量的范围和精度和 VS 中一样,当然这样对 PS 的要求比较高,所以在 PS 中,至少,float 类型的变量的范围至少为负 16384,到 16384,也就是负的 2 的 14 次方,到 2 的 14 次方。精度最少是 1024 分之 1。而 PS 中的 int 的范围,至少是负 2 的 10 次方,到 2 的 10 次方。
现在我们知道 VS 和 PS 中 float 和 int 的最小可支持范围和精度了。下面我们来解释精度修饰符。
精度修饰符一共有三种。从高到低分别是 highp、mediump、lowp。举个例子,比如 highp float x,就是定义了一个变量类型为 float 的 x,而这个 x 的范围 和精度为 highp。
highp
是 VS 支持的最低要求,也就是刚才所说的,float 类型的变量的范围至少为 负的 2 的 62 次方,到 2 的 62 次方。精度最少是 65536 分之 1。int 的范围,至 少是负的 2 的 16 次方,到 2 的 16 次方。
mediump
是 PS 支持的最低要求,也就是刚才所说的,float 类型的变量的范围至少为 负的 2 的 14 次方,到 2 的 14 次方。精度最少是 1024 分之 1。int 的范围,至少 是负的 2 的 10 次方,到 2 的 10 次方。
lowp
float 类型的变量的范围至少为负 2,到 2。精度最少是 256 分之 1。int 的范 围,至少是负的 2 的 8 次方,到 2 的 8 次方。
这些最小可支持范围只是说硬件必须支持这些范围和精度,而如果硬件做的更好,可以支持更大的范围和更小的精度,那么也没有问题。
如果在 shader 中使用了硬件不支持的范围和精度,比如在 PS 中,由于不要求硬件支持 highp,而刚好硬件确实不支持 highp。那么如果在 PS 中使用了 highp, 就会产生编译或者链接错误。
以上我们说了 VS 和 PS 中最基本的范围和精度,和三种精度修饰符所支持的最基本的范围和精度,而真正究竟支持的范围和精度是什么,每个平台都不同, 在 OpenGL ES 中可以通过 OpenGL ES 的 API 进行查询。
在 shader 中,也有一个预留的宏定义,如果 PS 也支持 highp。那么宏定义 GL_FRAGMENT_PRECISION_HIGH 的值被定义为 1,而如果不支持那么广的范围和高精度,则该宏定义则没有被定义,同时,由于 highp 这个修饰符在 PS 中是一个可选的特性,所以默认#extension 是把这个特性 disable 的。所以,如果想在 PS 中使用 highp,需要先通过#ifdef 判断宏定义 GL_FRAGMENT_PRECISION_HIGH 是否被定义,如果定义了,则通过#extension 把特性 enable。
数字常数和 bool 变量都没有精度修饰符。float 或者 int 的构造函数中的传参如果在声明构造函数的是时候没有写明精度修饰符,那么传入的参数就也没有精度修饰符。
一般情况下,经过操作的精度以及运算得到的结果的精度应该不低于运算时传入参数的精度,只是在少量的 buildin 的运算中,比如 atan,运算得到的结果的精度低于运算时传入参数的精度。
如果某个参加运算的参数没有精度修饰符,那么就以另外一个参加运算的参数的精度修饰符为准,如果都没有,那么就看下一个操作中的参数的精度修饰符。 以此类推,一直到找到一个精度修饰符为止。这里的下一个操作包括初始化赋值、 包括作为别的函数的传入参数、包括作为别的函数的返回参数。如果依然找不到一个精度修饰符,那么就认为当前的精度修饰符为默认值。一会我们将说一下什么是默认的精度修饰符。
如果一个 float 操作运算的结果超出了保存结果的变量的范围,那么结果可能是该变量范围的最大值或者表现成无穷大,反之,如果小于变量的范围,则结果可能是该变量范围的最小值或者表现成无穷小。同时,可能会导致越界、生成 NaN,或者异常。
类似的,如果 float 操作运算的结果太趋近于 0,以至于不在保存结果的变量的精度范围内,则结果为 0 或者是无穷小,但是如果表现成无穷小的话,正负符号一定要表达准确。
如果是 int 超出了范围,那么就会得到一个 undefine 值。
精度修饰符,类似于其他修饰符,不影响变量的基本变量类型。没有构造函数可以使得一个变量从一个精度变成另外一个精度,构造函数只能转换变量类型。
同样的,精度修饰符,和其他修饰符一样,也与函数重载无关,因为我们知道如果两个函数,函数名一致,也都只有一个参数,如果参数变量类型不同,那 么就算是重载,但是如果参数变量类型相同,只是修饰符不同,那么这不是重载。
还有,结构体的成员中,可以包含精度修饰符,但是不能包含别的修饰符。
在 struct 前面还可以加修饰符,但是修饰符并不属于这个自定义变量类型 struct 的一部分,它只用来修饰当前定义的这个变量。
刚才我们提到了默认的精度修饰符。
默认的精度修饰符是通过这个表达式创建的:precision 后面跟一个精度表达式比如 highp,mediump,lowp,在后面跟变量类型比如 float 或者 int 或者是 sample 类型(sample2D、sampleCube,因为我们提到过 Sample 类型原则上是 int 类型的变种)。如果使用其他修饰符或者变量类型都会导致出错。
那么这样,就确定了默认的精度表达式。
如果变量类型使用的是 float,那么所有 float 相关的变量类型,比如 vec2 或 者 mat3 等,定义的变量如果没有写明精度修饰符或者无法判断其精度修饰符, 那么就使用默认精度修饰符。
同样的,如果变量类型使用的是 int,影响的是所有 int 相关的变量类型,比 如 ivec2、ivec3 等。
这里说到的变量包含了局部变量,全局变量,函数传入参数、函数返回值。
默认精度修饰符也是有使用范围的,比如全局的,或者局部的。如果一个变量没有办法判断其精度修饰符,那么就使用最近的一个且在使用范围的默认精度修饰符。和变量一样,如果只是某个代码块定义的一个默认精度修饰符,那么出了这个代码块,就无效了。局部的默认精度修饰符在所在代码块中,会覆盖掉全局的默认精度修饰符,依此类推嵌套的代码块也是里面的在所在代码块中会覆盖掉外面的。如果同一个代码块中出现两个针对同一变量类型的默认精度修饰符, 则后写的那个会覆盖先写的的那个。
在 VS 中,针对 float,int,sample2d,sampleCube 都有 buildin 的默认全局精度修饰符,其中 float 和 int 都是 highp,sample2D 和 sampleCube 都是 lowp。 在 PS 中,只针对 int,sample2d,samplecube 有 buildin 的默认全局精度修 饰符,其中 int 为 mediump,sample2D 和 sampleCube 都是 lowp。PS 中 float 没 有 buildin 的默认全局精度修饰符,所以开发者要么把所有 float 相关变量类型定 义的变量全部加上精度修饰符,要么就自定义一个默认全局精度修饰符。比如, 在 PS 中,使用 precision lowp float;,也就是定义了这个 PS 中,所有的 float 如果 没有注明精度修饰符,那么默认是 lowp 的。
只有精度修饰符可以用来修饰一个函数的 return 变量。
恒定修饰符
最后一种修饰符,恒定修饰符。
首先先举个例子,假如有两个 vertex shader,在这两个 shader 中,使用同样的表达式对 gl_Position 进行赋值,而且表达式的输入参数完全一样,但是即使是这样,当这两个 shader 运行的时候,gl_Position 的值也有可能不同。这就是 shader 的特性,当然这里所说的不同,只是略微的不同,差别会非常小,一般情况下, 这种误差一般是可以接受的(除非是 multi-pass 等机制可能会造成差别, multi-pass 属于高级算法,这里我们就不做多的介绍)。
简单的解释一下,是这样的,在 shader 中,如果我们将一个变量定义成一个值,比如定义 a 为 3.0,那么 shader 并不会把 3.0 保存起来,而是在使用到 a 的时候,再根据场景重新计算,假如 a 的精度修饰符为 lowp,那么当它和 mediump 的 float 运算以及与和 lowp 的 float 运算,a 在这两次运算中的值会因为精度不同而不同。所以运算的结果也就不同。
invariant
而为了避免这种差别,变量可以被定义成 invariant。可以将某个变量定义成 invariant,也可以定义一个全局的 invariant,使得该 shader 中所有可以被定义成 invariant 的变量都定义成 invariant。
如果想将一个特定的变量定义成 invariant,有两种方式。一是将一个已经定义过的变量定义成 invariant,方法就是先定义一个变量,然后再用 invariant 恒定修饰符声明一遍,这种方式的话,invariant 后面可以跟多个使用逗号分割的已经定义过的变量;二是在定义变量的时候直接讲变量定义成 invariant,这种比较简 单,比如invariant varying medium vec3 Color;
这两种方式,是 invariant 恒定修饰符仅有的两种使用方式。并且只有如下几 种变量:VS 和 PS 的 buildin 的输出变量,PS 的 buildin 的输入变量,以及输出 VS 的 varying 和输入 PS 的 varying。
invariant 变量必须在变量被使用之前,就进行声明。
为了确保刚才那个例子中两个 shader 中特定的那个输出的恒定,以下几个条件必须满足:
- 两个 shader 中特定的那个输出必须要被声明成 invariant。
- 两个 shader 的输入参数必须完全一样,因为这些输入参数可能会被用于输出变量的赋值或者影响输出变量赋值的条件判断。
- shader 中所用到的所有纹理,包含纹理内容,纹理格式,纹理属性等信息必须完全一样,因为他们也可能对输出变量产生影响。
- 所有的输入参数都受到相同的操作。所有条件表达式和中间表达式中的操作都必须完全一致,使用的参数顺序一致,结合方式一致,就是看起来完全一致。中间变量和函数都必须定义成相同的类型(这里的类型包含精度修饰符),所有会影响这个输出值的流程都必须一致。
总的来说,就是所有的会影响到这个 invariant 输出变量的数据流和控制流都必须一致。
初始状态下,所有的输出变量都是 variant 的,如果想让所有的输出变量都变成 invariant 的,可以直接在 shader 任何声明之前,使用语法#pragma STDGL invaraint(all)。如果这个语法在变量或者函数声明之后,那么那些 invariant 的输出变量会变成 undefine。
invariant 其实是在放弃了优化,损失了性能的情况下做到的,所以一般用于 debug 使用。
常量表达式无论在哪里,结果都是恒定的,不管是在 VS 和 PS 中,或者两个不同的 VS 中,只要它们的输出参数一致,运算操作符一致,顺序一致,精度一致,那么得到的结果就是一致的。
被链接的一对 VS 和 PS,虽然精度修饰符可以不一致,但是恒定修饰符必须一致。
针对 buildin 的变量,只有在 gl_Position invariant 的时候,gl_FragCoord 才能 是 invariant;只有 gl_PointSize invariant 的时候,gl_PointCoord 才能是 invariant。 不能声明 gl_FrontFacing 是 invariant,因为它将听从 gl_Position 来确定其是否是 invariant。
以上我们讲述了一共四种修饰符,它们都可以用于声明变量,如果在一个变量中使用多个修饰符,那么这些修饰符必须有顺序,顺序是:
invariant-qualifier storage-qualifier precision-qualifier
storage-qualifier parameter-qualifier precision-qualifier
GLSL 的函数
截至到这里,GLSL 中变量的部分就已经全部讲完了,由于 GLSL 中的函数和 C、C++的非常类似,所以就不单独开新的一节对函数从语法角度进行讲解了。 就在这里,对 GLSL 中的函数稍微进行一点说明。
函数在使用之前要先进行定义。
函数的参数中如果有数组,那么数组一定要明确其长度。
如果函数会返回一个值,那么可以将调用该函数当作表达式,这个表达式的类型就是函数返回值的类型。
如果一个函数确定有输出函数,但是在函数体中没有写 return 语句,那么返回 undefine。
函数 input 和 output 主要是通过复制,复制的时候修饰符不一定必须完全一致。
GLSL 中的函数支持重载。重载的函数函数名一样,而传入参数的类型不同。
如果只是输出参数不同,或者传入参数的修饰符不同,则不算重载。
VS 和 PS 中都必须有一个 main 函数,main 函数的传入参数为空,也不做任何 return,输出为 void。函数中也可以使用 return,这样就会导致 main 函数提前结束。
在循环语句中,for 和 while 语句中都可以定义并初始化一个变量,但是do-while 却不行。
跳转语句:discard,discard 只能用在 PS 中,用于抛弃对当前像素的计算,由于 PS 之后还要做一些 test 然后更新 color buffer 等,而这个像素由于被抛弃了, 那么也就不会更新该像素点的 buffer 信息了。最常见的用法就是在 PS 中检测当前点的 alpha 是否小于 0,如果小于的话就会被抛弃。
关于函数的语法,也就先说明这么多。
本节教程就到此结束,希望大家继续阅读我之后的教程。
谢谢大家,再见!