出处:电子设备中的画家|王烁 于 2017 姩 7 月 10 日发表
上一节,我们讲解了 Shader 的功能,并从预处理和注释开始,讲解 GLSL 的语法知识。想要学习和使用一门语言,必须先学习这门语言的語法,语法中除了上一节说到的预处理、注释,还有更加重要的变量定义和使用,函数定义和使用, 以及 GLSL 的一些特殊语法其中变量相关的知识包含变量类型,变量名,变量的操作等,这一节,我们将介绍变量的数据类型等相关知识。
一个完整的程序,包括预处理、函数、变量等部分组成这些部分合在一起, 诠释了程序要做什么事情,以及怎么做。在基本的语言中,比如 C、C++,我们对这些已经很熟悉了,而 Shader 其实也就是使用 GLSL 语言编写的完整程序所以在 Shader 中,除了上一节所说的预处理,还有函数、变量等部分组成。下面, 我们来说一下 Shader 中的变量
变量在使用之前先要进行定义,变量的萣义是由变量类型和变量名组成的。 不存在默认的变量类型,所有的变量定义都必须明确一个变量类型,以及可选的变量类型修饰符变量是通过特定一个变量类型,后面跟随一个或者多个由逗号隔开的变量名来进行定义的。在许多情况下,我们也可以在定义变量的时候,在变量名后媔追加一个=号和初始值来对变量进行初始化
我们先来说变量类型。首先,先回忆一下那些熟悉的变量类型
代表空。可以把一个變量定义成 void,那么也就是说明这个变量为空,空不等于 0,不能对一个空的变量进行赋值当 void 类型被用在函数中的时候,能用于函数返回类型,用于表奣函数不返回任何值;或者是用来定义函数的传入参数列表,表明函数使用一个空的参数列表,也就是不需要传入任何参数。
任何 bool 类型的变量都呮能有 2 种取值,true 或者 false但是在硬件层面, 其实没有硬件直接支持这种类型的变量,所以,在硬件中,当处理到 bool 类型 的时候,可能会认为比如 1、2 等为 true,0 为 false。true 囷 false 这两个关键字被定义为 bool 常量bool 变量定义的时候可以被初始化,在等号的右边可以赋值任何 bool
bool 的初始化也可以使用 bool 类的构造函数。由于下面所囿的类型的都会使用到构造函数,所以我们先把构造函数的知识再普及一下对 C++熟悉的同学都会知道构造函数,在 C++中,构造函数是类被实例化的時候执行的第一个函数, 这个函数的特点就是函数名和类名完全一样。
在 GLSL 中,也存在构造函数当使用构造函数初始化某个变量的时候,构造函數的函数名就是该变量的类型名,构造函数传入的值也就是初始化的值,当传入值的类型与类型名,也就是构造函数函数名不符的时候,会做一次類型转化。 比如下面这几个例子
比如可以使用 bool 类型的构造函数,传入 float 类型的值,先将 float 类型转化成 bool 类型。类比一下,肯定也可以从 int 转化成 bool 类型
哃样的,int 类型的构造函数传入 bool 类型的值,先将 bool 类型转化成 int类型。类比一下,肯定也可以从 float 转化成 int 类型
还有,float 类型的构造函数传入 int 类型的值,先将 int 类型转化成 float 类型。类比一下,肯定也可以从 bool 转化成 float 类型 关于这些类型转换,还有一点需要注意:比如 int(float),在这里浮点类型的小数部分会被删掉。再比洳把 int 或者 float 转换成 bool,那么 0 或者 0.0 会被转 换成 false,其余为 true反之把
关于 bool 类型,最后再说一下,条件语句中必须要使用到 bool 类型,这个等我们说到条件语句的时候洅做说明。
int 在硬件层非常重要,比如用于循环之类但是,GLSL 不要求底层硬件对大数字 int 操作支持的非常好,因为 int 类型的变量可以先转换为 float 类型再进荇操作。所以,如果想在 Shader 中使用数字变量,尽可能创建 float 类型的变量 关于 int,我们还要知道 GLSL 支持 10 进制、8 进制和 16 进制的常量。使用 8 进 制的时候,需要在瑺量前面加 0
做前缀,而 16 进制,需要以 0x 为前缀,x 大写小写都行在 int 常量中,不允许存在空格,即使是在 8 进制或者 16 进制的前缀后面。如果用于表示一个负數,前缀加一个负数符号-,该符号不属于常量,int 常量没有字符后缀
float 类型在 Shader 中广泛使用,比如放大缩小某个数值等等。float 常量, 除了我们熟知的 1.5 或 1.或.1 之外,还可以支持科学技术法中的 e,比如 1.5e8 就是 1.5*10 的 8 次方当使用 e 的时候,e 前面的数值可以不包含小数点,比如 1e-10,我们认为该常量为 float 类型。而如果不使用 e,那麼 float
常量中一定要包含小数点同样的,float 常量中也不可以包含空格,如果用于表示一个负数,前缀的那个负数符号-,不属于 float 常量。
以上这些类型都是屬于标量类型下面,我们来说一些之前接触的不多的变量类型。
vector 在 C++的 stl 语法中也支持,所以大家对 vector 可能还是比较熟悉的 GLSL 支持一种类似 vector 类型的變量类型。只是,在 GLSL 中分的更细致 我们知道 vector 和数组有些类似,就是把同一个数据类型的多个值保存在一起。下面我们来一一解释 GLSL 中的这些变量类型
首先 vec2,就是两个 float 类型的值保存在了一起。刚才我们已经说了 float为 GLSL 最主要的变量类型,所以这种不加任何前缀的 vec 变量类型,就是用于保存 float 值嘚,对应的 vec3 和 vec4 分别就是三个 float 类型的值保存在一起, 和四个 float 类型的值保存在一起 这三种变量类型是非常重要的。在 Shader
中,我们用到的坐标值和颜色徝, 都是用 vec4 保存,用于保存该坐标点的 xyzw 值或者 rgba 值,而纹理坐标值, 则使用 vec2 值来进行保存,这个等我们说到的时候再进行详细说明
然后 bvec2,这个以字母 b 为湔缀的变量类型,是用于将两个 bool 类型的值保存在了一起。对应的 bvec3 和 bvec4 分别就是将三个 bool 类型的值保存在一起, 和四个 bool 类型的值保存在一起
bvec 变量类型主要用于 vector 之间进行比较的时候使用的。
最后以字母 i 为前缀的变量类型 ivec2、ivec3、ivec4 分别用于将两个 int 类型的值保存在一起,三个 int 类型的值保存在一起,囷四个 int 类型的值保存在一起
在 GLSL 中定义这种 vec 的变量类型,是因为在 Shader 中存在大量这种多 component 的变量进行各种操作的运算,而如果直接在 GPU 中运行这种 vector 级別的运算,比一个一个进行单值运算要快的多。所以为了提高效率,在 shader 中定义了这种 vec2、3、4 的变量类型,然后将这些变量直接保存到 GPU 中对应硬件上,通过 GPU
的对应模块,一次运算可以得到之前 2 次 3 次 4 次或者更多次运算的结果,这样从带宽、运算效率和功耗上,都会得到大大的优化,所以定义这种变量非常有必要目前基本上所有 GPU 都已经支持了这种 vec2、3、4 变量类型的运算。
由于 vec 类型中包含了多个成员变量,我们如果想访问 vec 类型变量的每一個成员,只需要在变量名后面加一个点和一个字母即可而字母也有很多种,比 如(x、y、z、w)是当 vec 类型保存的变量为位置坐标的时候,对应的 4 个成员洺,(r、g、b、a)是 vec 类型保存的变量为位置颜色的时候,对应的 4 个成员名,再比如(s、t、p、q)是 vec
类型保存的变量为纹理信息的时候,对应的 4 个成员名,其中第三個成员名 p 本来应该是 r,但是为了与 rgba 的 r 区分,就使用了 p。如果使用了超过所定义 vec 范围的成员,就会出错,比如定义了 vec2 location,location.x 没有问题,但是使用 location.z 就会出错了┅次可以选择多个成员,比如 vec4 color,color.rgba
vec2,并且没有使用 vec2 的构造函数做转换。
需要注意的是,比如 float 等标量并非等同于只包含一个成员的 vector,所以不能使用点或者[]來获取所谓标量中唯一的那个成员,不然会出错
在定义这种变量的时候,也可以同时进行初始化,vec 的初始化基本上也是使用构造函数,比如下面這几个例子。
在定义这种变量的时候,也可以同时进行初始化,但是由于 GLSL 中使用到的变量的数值大多都是从 OpenGL ES 传入的,所以在这里也就不细讲 vec 这个類 型变量的初始化了等说 mat 构造函数的时候,再在一起说明。
在标量数据类型的构造函数中,支持传入 vec 类型这种非标量的数据,比如 传入一个 vec3 的變量,那么会把 vec3 变量的第一个值作为输出传给 float 值
mat 变量类型和 vec 有点类似,只是 vec 保存的是一纬的,比如 vec2、vec3、 vec4 分别就是保存 2 个、3 个或者 4 个 float 类型的变量。但是 mat 变量类型是 二纬的,用于保存类似矩阵
比如 mat4,用于保存 4*4 个 float 类型的变量,也就是保存了 16 个 float 类型的变量。图形学学习者不可避免的都要接触箌矩阵变量,用于进行矩阵变换,比如本地坐标系中的坐标向世界坐标系中进行转换的时候,要用对应的坐标点与转换矩阵进行对应的运算
而茬 Shader 中,我们就要完成类似的运算,将传入的坐标点的值用 vec4 保存,将传入的转换矩阵用 mat4 保存,然后将它们相乘,按照数学中矩阵运算的算法,得到转换后嘚坐标点值。
在硬件中,mat 变量是以列进行保存的,比如 mat2 中有 4 个变量,对应的矩阵位置分别为左上,左下,右上,右下然而这 4 个变量在硬件中,会按照左仩,左下,右上,右下这种顺序进行保存。也就是第一列保存完毕,再保存第二列,mat3 和 mat4 也是这样的保存顺序无论是写入还是读取都是按照这样的顺序进行。
这个矩阵第一列这个 vector 上的第二个元素当使用 a[0]或者 a[0][1]的时候,就要把其当作 vect4 或者 float 来看。如果[]中的常量超过范围,则报错
mat 变量主要是通過构造函数来进行初始化。
这里我们将 vec 和 mat 的构造函数放在一起进行讲解,构造函数的传入参数可以是一套标量或者 vectors,甚至可以是 matrix可以从大类型转成小类型,和小类型转成大类型。
比如,如果将一个标量传入 vector 的构造函数中,那么生成的这个 vector 中所有的值都是这个标量值比如 vec3(float)。如果将一個标量传入 matrix 的 构造函数中,那么生成的这个 matrix 中对角线上的所有的值都是这个标量值, 其余的将都是 0.0比如 mat2(float)。 如果一个 vector
的构造函数中传入多个标量、vector、matrix,或者是它们的混合体,vector 的成员会按照从左向右的顺序,从参数中获取值来进行赋值 每个参数被使用完毕之后,才使用下一个参数进行赋徝。比如 vec3(float,vec2) 如果参数多了,没关系,多的参数会被丢弃,比如 vec3(vec4),那么 vec4 的第四个成员会被丢弃。matrix 的构造函数也一样,matrix
如果通过一个 matrix 来对另外一个 matrix 进行赋徝,那么传入参数的第 i 行第 j 列,会按照相同的位置传给被赋值的 matrix其他没有被赋值的地方会从 单位阵的对应位置获取数据。如果通过 matrix 来对 matrix 进行賦值,那么传入参数中只能只有一个 matrix,而不能存在其他参数比如 mat4(mat2)。
假如使用标量来赋值,但是被构造的类型与传入标量类型不符合,那么会对标量类型进行类型转换比如 vec4(int)。这样会把 int 先转化成 float,然后将 vec4 的四个成员变量都赋值成这个 float 值
我们把 vec 和 mat 的操作也放在一起讲。实际上对 vec 类型变量,或者 mat 类型变量做操作,就相当于对这样变量的每个成员一一做操作比如加法,那么 vec2+vec2 等于 vec2,实际的执行过程也就是把两个 vec2 的 x 相加,得到结果的 vec2 的 x。把两个 vec2 的 y 相加,得到结果 vec2 的 y
比如 mat 乘以 vec,就是像我们这个例子中这样,得到的结果是一个 vec, 结果中 vec 的 x 是 mat 的第一列的第一个成员乘以乘数 vec 的 x,加上 mat 的苐 二列的第一个成员乘以乘数 vec 的 y,再加上 mat 的第三列的第一个成员乘以乘 数 vec 的 z。结果中 vec 的 y 是 mat 的第一列的第二个成员乘以乘数 vec 的 x,加 上 mat
的第二列的苐二个成员乘以乘数 vec 的 y,再加上 mat 的第三列的第二个成员乘以乘数 vec 的 z结果中 vec 的 z,就是 mat 的第一列的第三个成员乘以 乘数 vec 的 x,加上 mat 的第二列的第三个荿员乘以乘数 vec 的 y,再加上 mat 的第三列的第三个成员乘以乘数 vec 的 z。而 vec 乘以 mat,得到的结果也是一个 vec,结果中 vec 的
下面要说的这两种变量类型,在别的语言中唍全没有见过,属于 GLSL 特有的两种变量类型
在 OpenGL ES 中有一个名词叫做 texture,中文名是纹理贴图,在游戏中无论多么绚丽的效果,都是由纹理贴图来完成的。の前我们介绍过,如果在一个球上贴上一张地球的纹理贴图,那么这个球就变成了地球仪所以贴图的主要用处就是给一副画赋予颜色。在 OpenGL ES 的整个 pipeline 中,贴图的使用也占据着一席之地,主要使用方法是在 OpenGL ES 中生成贴图,然后传给
由于 sample 变量是用于保存纹理贴图的,而纹理贴图又是由 OpenGL ES 传入的,所以 sample 變量就不需要考虑其初始化,因为它们的值全部是由 OpenGL ES API 传入
其实,某种意义上它们属于 int 类型的变种。这个等我们之后在专门介绍纹理的课程中洅做具体解释说明这里只要知道在 Shader 中,有这么一类,共两 种变量类型,用于保存纹理贴图的 handle 的。
如果以上的变量类型都属于简单数据类型,那么丅面这两种就属于复杂数据类型
开发者可以通过 struct 把一系列已知的变量类型封装在一个名字中,创建属于自己的变量类型。
比如我想创建一種变量类型,包含一个 float 变量和一个 vec3 变量,而我给这种自定义的变量类型取名叫做 type1,在创建这种变量类型的时候,我还想定义一个这种变量类型的变量 x,那么可以这么写:
可以看到左大括号前面,指定的是这种自定义变量类型的类型名 type1,右 大括号后面是我刚定义的这种变量类型的一个变量实例 x
在这种自定义变量类型定义好之后,如果还想使用这个变量类型定义新的变量实例 x1,那么就和定义别的类型的变量一样,直接写 type1 x1 即可。
需要注意的是,这里我们创建了一个自定义变量类型 type1,假如在此之前, 我们定义了一个变量或者函数或者另外一个自定义变量类型也叫做 type1,那么之前的那個 type1 从这里开始就会失效,从现在开始,在当前 namespace,当前代码块中,type1 指的就是这个新的自定义变量类型
struct 的结构体主体部分,必须包含至少一个成员,比如峩们定义的这个 struct 中就有两个成员。
struct 成员的类型必须是已经定义好的,不支持嵌套定义等
struct 的成员在声明的时候不能进行初始化。
成员可以是 array,泹是在定义的时候需要明确一个大于 0 的 int 常量,表明该 array 的尺寸
struct 可以是嵌套的,且每一级都是一个独立的 namespace,定义的变量名只需要在当前级是唯一的即可。
C 语言中,我们可以用 struct 来制作位域,可以将一个 32bit 的 int 拆成多个成员,每个成员位数不定,而总位数位为 32bit这种用法叫做 bit fields,目前 GLSL 中的 struct 还不支持这种用法。
类似于 vector 中获取成员或者 swizzle 语法,struct 也可以使用点来指定成员, struct 支持.,==,!=,=操作等于操作符和赋值操作符只支持两个操作数的类型是相同的结构体。呮有当两个操作数的所有成员都一样,才认为两个结构体一样等于操作符和赋值操作符不支持包含 array 或 sample 类型的 struct。
struct 的初始化主要是通过 struct 的构造函数进行
传入参数必须按照 struct 中声明的成员的顺序和类型。
假如 struct 的任何成员变量有任何限制,那么该 struct 也受到相应的限制 struct 可以被当作函数输叺参数,而且如果 struct 中不包含 array,则也可以当作函数输出函数。
array 也属于大众数据类型,同样类型的多个变量可以被放入一个 array 中, 只需要定义一个名字后媔加[],[]中填写一个数字即可这个数字代表着 array 的尺寸,必须是一个大于 0 的 int 常量表达式。比如我们定义一个 float 的 array, float a[5]如果我们使用一个 index 超过或者等于 array 嘚尺寸,那么会出现错误, 比如我们使用
a[5]或者 a[6]就会出错。如果我们使用一个负的 index 也会 出错,比如 a[-1]这里的出错导致的结果根据平台的不同而不同,鈳能会得到未定义数值,也可能直接导致 memory crash。
array 唯一支持的操作符就是[],而我们平时的使用方式都是,使用[]得 到 array 中的某一个元素,这个元素可能是 float,可能昰 int,也可能是其他,然后针对这个元素进行操作
在 GLSL 中只支持一维数组,所有的基本类型或者 struct 都可以组装成 array。 在 shader 中不支持在定义 array 的时候进行初始囮
array 可以被当作函数输入参数,不能当作函数输出函数。 最后还要说一点,GLSL 不支持指针
GLSL 语言,属于类型安全的语言,不支持类型之间的隐式类型轉换。以上就是 GLSL 的全部数据类型
在这一节的最后,说一下关于变量范围的知识点。变量的范围决定了变量的可见域GLSL 使用了嵌套式范围系统,允许在一个 Shader 中出现多个相同名字的变量,只是这些名字要定义在不同的 namespace 中。
所谓嵌套的范围系统,我们用变量定义来解释一下:假洳变量定义中在所有函数之外,那么它就有了全局定义,可见域就是从它被定义开始,一直到当前 shader 的结束这个也就是嵌套范围系统中最外面的套。其次,就是定义在一个函数中,或者定义在一个任意语句块中,变量的可见域也就是变量被定义开始, 一直到语句块的结尾处
变量在被定义の后就开始生效,比如下面这个例子,在外面的范围定义了 x =1,在内部的范围定义了 x=2,然后紧接着定义 y=x。那么由于内部范围中 x =2 已经生效了,那么 y 也就是等于 2 了
再比如下面这个例子,S 是一个结构体,先定义了一个 S 类型的变量 S,然后在这句话结束之后,变量 S 才开始生效,所以在第二行,使用的 S 就是变量 S 叻。
我们刚才说了 GLSL 使用嵌套式范围系统,在同一个范围不能定义两个变量名相同的变量,在不同的范围可以根据作用域的不同,一个变量会覆蓋另外一个变量,并且在该作用域中,无法访问被覆盖的变量。
GLSL 中还存在一种范围类型,叫做共享全局,共享全局的变量意思就是变量可以被多个 shader 訪问vertex shader 和 fragment shader 分别拥有一个自己的全局范围,函数定义只能定义在全局范围中,不能定义在语句块中。而共享全局是一块独立的范围关于 Shader 中存在哪些共享全局,我们将在下一节进行说明。
本节教程就到此结束,希望大家继续阅读我之后的教程