《OpenGL编程指南(原书第9版)》——2.3 OpenGL着色语言概述

简介: 本节书摘来自华章计算机《OpenGL编程指南(原书第9版)》一书中的第2章,第2.3节,作者:(美)约翰·克赛尼希(John Kessenich)著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.3 OpenGL着色语言概述

本节将会对OpenGL中着色语言的使用进行一个概述。GLSL具备了C++和Java的很多特性,它也被OpenGL所有阶段中使用的着色器所支持,尽管不同类型的着色器也会有一些专属特性。我们首先介绍GLSL的需求、类型,以及其他所有着色阶段所共有的语言特性,然后对每种类型的着色器中的专属特性进行讨论。
2.3.1 使用GLSL构建着色器
我们将在这里介绍如何创建一个完整的着色器。
从这里出发
一个着色器程序和一个C程序类似,都是从main()函数开始执行的。每个GLSL着色器程序一开始都如下所示:
image

这里的//是注释符号,它到当前行的末尾结束,这一点与C语言一致。此外,着色器程序也支持C语言形式的多行注释符号—//。但是,与ANSI C语言不同,这里的main()函数不需要返回一个整数值,它被声明为void。此外,着色器程序与C语言以及衍生的各种语言相同,每一行的结尾都必须有一个分号。这里给出的GLSL程序绝对合法,可以直接编译甚至运行,但是它的功能目前还是空白。为了能够进一步丰富着色器代码中的内容,下面将进一步介绍变量的概念以及相关的操作。
变量的声明
GLSL是一种强类型语言,所有变量都必须事先声明,并且要给出变量的类型。变量名称的命名规范与C语言相同:可以使用字母、数字,以及下划线字符(_)来组成变量的名字。但是数字不能作为变量名称的第一个字符。此外,变量名称也不能包含连续的下划线(这些名称是GLSL保留使用的)。
表2-1中给出了GLSL支持的基本数据类型。
这些类型(以及后文中它们的聚合类型)都是透明的。也就是说,它们的内部形式都是暴露出来的,因此着色器代码中可以假设其内部的构成方式。
与之对应的一部分类型,称作不透明类型,它们的内部形式没有暴露出来。这些类型包括采样器(sampler)、图像(image),以及原子计数器(atomic counter)。它们所声明的变量相当于一个不透明的句柄,可以用来读取纹理贴图、图像,以及原子计数器数据,参见第4章。image

不同的采样器类型以及它们的应用可以参见第6章。
变量的作用域
虽然所有的变量都需要声明,但是我们可以在使用它们之前的任何时候声明这些变量(这一点与C++一致)。我们可以对照C++的语法来了解GLSL变量的作用域规则,如下
所示:
在任何函数定义之外声明的变量拥有全局作用域,因此对着色器程序中的所有函数都是可见的。
在一组大括号之内(例如函数定义、循环或者“if”引领的代码块等)声明的变量,只能在大括号的范围内存在。
循环的迭代自变量,例如,下面的循环中的i:
image

只能在循环体内起作用。
变量的初始化
所有变量都必须在声明的同时进行初始化。例如:
image

整型字面量常数可以表示为八进制、十进制或者十六进制的值。我们也可以在数字之前加上一个符号来表示负数,或者在末尾添加“u”或者“U”来表示一个无符号的整数。
浮点字面量必须包含一个小数点,除非我们用科学计数法来表示它,例如3E-7(不过,很多时候我们也可以将一个整数隐式地转换为一个浮点数)。此外,浮点数也可以选择在末尾添加一个“f”或者“F”后缀,这一点与C语言中浮点数的表示法相同。如果要表达一个double精度的浮点数,必须在末尾添加后缀“lF”或者“LF”。
布尔变量可以是true或者false,对它进行初始化的时候,可以直接指定这两个值之一,也可以对一个布尔表达式进行解析并且将结果赋予变量。
构造函数
正如前面提到的,GLSL比C++更注重类型安全,因此它支持的数值隐式转换更少一些。例如,
image

这样的写法会返回一个编译错误,因为布尔值不能赋予整型变量。可以进行隐式转换的类型如表2-2所示。image

上面的类型转换适用于这些类型的标量、向量以及矩阵。转换不会改变向量或者矩阵本身的形式,也不会改变它们的组成元素数量。类型转换不能应用于数组或者结构体之上。
所有其他的数值转换都需要提供显式的转换构造函数。这里构造函数的意义与C++等语言类似,它是一个名字与类型名称相同的函数,返回值就是对应类型的值。例如,
image

这里用到了一个int转换构造函数来完成转换。此外,其他一些类型也有转换构造函数,包括f?loat、double、uint、bool,以及这些类型的向量和矩阵。每种构造函数都可以传入多个其他类型的值并且进行显式转换。这些函数也体现了GLSL的另一个特性:函数重载,即每个函数都可以接受不同类型的输入,但是它们都使用了同一个基函数名称。我们稍后将对函数进行更多的讲解。
聚合类型
GLSL的基本类型可以进行合并,从而与核心OpenGL的数据类型相匹配,以及简化计算过程的操作。
首先,GLSL支持2个、3个以及4个分量的向量,每个分量都可以使用bool、int、uint、f?loat和double这些基本类型。此外,GLSL也支持f?loat和double类型的矩阵。表2-3给出了所有可用的向量和矩阵类型。
image

矩阵类型需要给出两个维度的信息,例如mat4x3,其中第一个值表示列数,第二个值表示行数。
使用这些类型声明的变量的初始化过程与它们的标量部分是类似的:
image

类型之间也可以进行等价转换:
image

向量的构造函数还可以用来截短或者加长一个向量。如果将一个较长的向量传递给一个较短向量的构造函数,那么向量将被自动取短到对应的长度。
image

类似地,也可以使用同样的方式来加长一个向量。这也是唯一的一类构造函数,它的输入参数比变量的实际分量数更少。
image

矩阵的构建方式与此相同,并且可以将它初始化为一个对角矩阵或者完全填充的矩阵。对于对角矩阵,只需要向构造函数传递一个值,矩阵的对角线元素就设置为这个值,其他元素全部设置为0,例如:
image

矩阵也可以通过在构造函数中指定每一个元素的值来构建。传入元素可以是标量和向量的集合,只要给定足够数量的数据即可,每一列的设置方式也遵循这样的原则。此外,矩阵的指定需要遵循列主序的原则,也就是说,传入的数据将首先填充列,然后填充行(这一点与C语言中二维数组的初始化是相反的)。
例如,可以通过下面几种形式之一来初始化一个3×3的矩阵:

image

访问向量和矩阵中的元素
向量与矩阵中的元素是可以单独访问和设置的。向量支持两种类型的元素访问方式:使用分量的名称,或者数组访问的形式。矩阵可以以二维数组的形式进行访问。
向量中的各个分量是可以通过名称进行访问的,例如:
image

或者通过一个从0开始的索引。下面的代码与上面的结果是完全等价的:
image

事实上,正如表2-4所示,分量的名称总共有三种形式的集合,它们实现的工作是一样的。不同的名称集合只是为了在使用时便于区分不同的操作。image

这种分量访问符的一个常见应用叫做swizzle,对于颜色的处理,比如颜色空间的转换时可能会用到它。例如,可以通过下面的代码,基于输入颜色的红色分量来设置一个亮度值:
image

类似地,如果需要改变向量中分量各自的位置,可以这样做:
image

唯一的限制是,在一条语句的一个变量中,只能使用一种类型的访问符。也就是说,下面的代码是错误的:
image

此外,如果我们访问的元素超出了变量类型的范围,也会引发编译时错误。例如:
image

矩阵元素的访问可以使用数组标记方式。或者从矩阵中直接得到一个标量值,或者一组元素:
image

结构体
你也可以从逻辑上将不同类型的数据组合到一个结构体当中。结构体可以简化多组数据传入函数的过程。如果定义了一个结构体,那么它会自动创建一个新类型,并且隐式定义一个构造函数,将各种类型的结构体元素作为输入参数。
image

与C语言中的用法类似,如果我们需要引用结构体的某个元素,可以直接使用“点”(.)符号。
数组
GLSL还支持任意类型的数组,包括结构体数组。和C语言相同,数组的索引可以通过方括号来完成([ ])。一个大小为n的数组的元素范围是0到n-1。但是与C语言中不同的是,负数形式的数组索引,或者超出范围的索引值都是不允许的。GLSL 4.3中,数组的组成元素也可以是另一个数组,因此可以处理多维度的数据。不过,GLSL 4.2和更早的版本不允许建立数组类型的数组(因此无法创建多维度的数组)。
数组可以定义为有大小的,或者没有大小的。我们可以使用没有大小的数组作为一个数组变量的前置声明,然后重新用一个合适的大小来声明它。数组的声明需要用到方括号的形式,例如:
image

数组属于GLSL中的第一等(f?irst-class)类型,也就是说它有构造函数,并且可以用作函数的参数和返回类型。如果我们要静态初始化一个数组的值,那么可以按照下面的形式来使用构造函数:
image

这里构造函数的维数值可以不填。
此外,GLSL的数组与Java类似,它有一个隐式的方法可以返回元素的个数:即取长度的方法length()。如果我们需要操作一个数组中所有的值,可以根据下面的例子来使用length()方法:
image

向量和矩阵类型也可以使用length()方法。向量的长度也就是它包含的分量的个数,矩阵的长度是它包含的列的个数。事实上,当我们使用数组的形式来索引向量和矩阵的值的时候(例如,m[2]是矩阵m的第三列),这个方法返回的就是我们需要的数据。
mat3x4 m;
int c = m.length(); // m包含的列数为3
int r = m[0].length(); // 第0个列向量中分量的个数为4
因为长度值在编译时就是已知的,所以length()方法会返回一个编译时常量,我们可以在需要使用常量的场合直接使用它,例如:
mat4 m;
f?loat diagonal[m.length()]; // 设置数组的大小与矩阵大小相等
f?loat x[gl_in.length()]; // 设置数组的大小与几何着色器的输入顶点数相等
对于所有向量和矩阵,以及大部分的数组来说,length()都是一个编译时就已知的常量。但是对于某些数组来说,length()的值在链接之前可能都是未知的。如果使用链接器来减少同一阶段中多个着色器的大小,那么可能发生这种情况。对于着色器中保存的缓存对象(使用buffer来进行声明,后文将会介绍),length()的值直到渲染时才可能得到。如果我们需要length()返回一个编译时常量,那么我们需要保证着色器中的数组大小在使用它的length()方法之前就已经确定了。
多维数组相当于从数组中再创建数组,它的语法与C语言当中类似:
f?loat coeff3; // 一个大小为3的数组,其中包含了大小为5的多个数组
coeff2 *= 2.0; // 内层索引设置为1,外层设置为2
coeff.length(); // 这个方法会返回常量3
coeff[2]; // 这是一个大小为5的一维数组
coeff[2].length(); // 这个方法会返回常量5
多维数组可以使用任何类型或者形式来构成。如果需要与应用程序共享,那么最内层(最右侧)维度的数据在内存布局中的变化是最快的。
2.3.2 存储限制符
数据类型也可以通过一些修饰符来改变自己的行为。GLSL中一共定义了几种全局范围内的修饰符,如表2-5所示。
image

const存储限制符
与C语言中相同,const类型的修饰符设置变量为只读类型。例如,下面的语句
image

会设置一个变量Pi为圆周率π的近似值。对变量的声明添加了const修饰符之后,如果再向这个变量写入,那么将会产生一个错误,因此这种变量必须在声明的时候就进行初
始化。
in存储限制符
in修饰符用于定义着色器阶段的输入变量。这类输入变量可以是顶点属性(对于顶点着色器),或者前一个着色器阶段的输出变量。
片元着色器也可以使用一些其他的关键词来限定自己的输入变量,这会在第4章中进行讲解。
out存储限制符
out修饰符用于定义一个着色器阶段的输出变量—例如,顶点着色器中输出变换后的齐次坐标,或者片元着色器中输出的最终片元颜色。
uniform存储限制符
在着色器运行之前,uniform修饰符可以指定一个在应用程序中设置好的变量,它不会在图元处理的过程中发生变化。uniform变量在所有可用的着色阶段之间都是共享的,它必须定义为全局变量。任何类型的变量(包括结构体和数组)都可以设置为uniform变量。着色器无法写入到uniform变量,也无法改变它的值。
举例来说,我们可能需要设置一个给图元着色的颜色值。此时可以声明一个uniform变量,将颜色值信息传递到着色器当中。而着色器中会进行如下声明:
image

在着色器中,可以根据名字BaseColor来引用这个变量,但是如果需要在用户应用程序中设置它的值,还需要多做一些工作。GLSL编译器会在链接着色器程序时创建一个uniform变量列表。如果需要设置应用程序中BaseColor的值,我们需要首先获得BaseColor在列表中的索引,这一步可以通过glGetUniformLocation()函数来完成。
GLint glGetUniformLocation(GLuint program, const char* name);
返回着色器程序中uniform变量name对应的索引值。name是一个以NULL结尾的字符串,不存在空格。如果name与启用的着色器程序中的所有uniform变量都不相符,或者name是一个内部保留的着色器变量名称(例如,以gl_开头的变量),那么返回值为-1。
name可以是单一的变量名称、数组中的一个元素(此时name主要包含方括号以及对应的索引数字),或者结构体的域变量(设置name时,需要在结构体变量名称之后添加“.”符号,再添加域变量名称,并与着色器程序中的写法一致)。对于uniform变量数组,也可以只通过指定数组的名称来获取数组中的第一个元素(例如,直接用“arrayName”),或者也可以通过指定索引值来获取数组的第一个元素(例如,写作“arrayName[0]”)。
除非我们重新链接着色器程序(参见glLinkProgram()),否则这里的返回值不会发生变化。
当得到uniform变量的对应索引值之后,我们就可以通过glUniform()或者glUniform-Matrix()系列函数来设置uniform变量的值了。
例2.2是一个获取uniform变量的索引并且设置具体值的示例。
例2.2 获取uniform变量的索引并且设置具体值

void glUniform{1234}{fdi ui}(GLint location, TYPE value);
void glUniform{1234}{fdi ui}v(GLint location, GLsizei count, const TYPE* values);
void glUniformMatrix{234}{fd}v(GLint location, GLsizei count, GLboolean transpose, const GLf?loat* values);
void glUniformMatrix{2x3,2x4,3x2,3x4,4x2,4x3}{fd}v(GLint location, GLsizei count, GLboolean transpose, const GLf?loat* values);

设置与location索引位置对应的uniform变量的值。其中向量形式的函数会载入count个数据的集合(根据glUniform*()的调用方式,读入1~4个值),并写入location位置的uniform变量。如果location是数组的起始索引值,那么数组之后的连续count个元素都会被载入。
GLf?loat形式的函数(后缀中有f)可以用来载入单精度类型的浮点数、f?loat类型的向量、f?loat类型的数组、或者f?loat类型的向量数组。与之类似,GLdouble形式的函数(后缀中有d)可以用来载入双精度类型的标量、向量和数组。GLf?loat形式的函数也可以载入布尔数据。
GLint形式的函数(后缀中有i)可以用来更新单个有符号整型、有符号整型向量、有符号整型数组,或者有符号整型向量数组。此外,可以用这种形式载入独立纹理采样器或者纹理数组、布尔类型的标量、向量和数组。与之类似,GLuint形式的函数(后缀中有ui)也可以用来载入无符号整型标量、向量和数组。
对于glUniformMatrix{234}*()系列函数来说,可以从values中读入2×2、3×3或者4×4个值来构成矩阵。
对于glUniformMatrix{2x3,2x4,3x2,3x4,4x2,4x3}*()系列函数来说,可以从values中读入对应矩阵维度的数值并构成矩阵。如果transpose设置为GL_TRUE,那么values中的数据是以行主序的顺序读入的(与C语言中的数组类似),如果是GL_FALSE,那么values中的数据是以列主序的顺序读入的。
buffer存储限制符
如果需要在应用程序中共享一大块缓存给着色器,那么最好的方法是使用buffer变量。它与uniform变量非常类似,不过也可以用着色器对它的内容进行修改。通常来说,需要在一个buffer块中使用buffer变量,本章后面将对“块”的概念进行介绍。
buffer修饰符指定随后的块作为着色器与应用程序共享的一块内存缓存。这块缓存对于着色器来说是可读的也是可写的。缓存的大小可以在着色器编译和程序链接完成后设置。
shared存储限制符
shared修饰符只能用于计算着色器当中,它可以建立本地工作组内共享的内存。第12章会详细介绍它。
2.3.3 语句
着色器的真正工作是计算数值以及完成一些决策工作。与C++中的形式类似,GLSL也提供了大量的操作符,来实现各种数值计算所需的算术操作,以及一系列控制着色器运行的逻辑操作。
算术操作符
任何一种语言的教程如果缺少有关操作符以及优先级的介绍(参见表2-6),那么这个教程是不完整的。表2-6中操作符的优先级采取降序排列。总体上来说,操作符对应的类型必须是相同的,并且对于向量和矩阵而言,操作符的操作对象也必须是同一维度的。在表2-6中注明的整型包括int和uint,以及对应的向量;浮点数类型包括f?loat和double,以及对应的向量与矩阵;算术类型包括所有的整型和浮点数类型,以及所有相关的结构体和数组。
image
image

操作符重载
GLSL中的大部分操作符都是经过重载的,也就是说它们可以用于多种类型的数据操作。特别是,矩阵和向量的算术操作符(包括前置和后置的递增/递减符号“++”和“--”)在GLSL中都是经过严格定义的。例如,如果我们需要进行向量和矩阵之间的乘法(注意,操作数的顺序非常重要,从数学上来说,矩阵乘法是不遵循交换律的),可以使用下面的操作:
image

基本的限制条件是要求矩阵和向量的维度必须是匹配的。此外,也可以对向量或者矩阵执行标量乘法,以得到希望的结果。一个必须要提及的例外是,两个向量相乘得到的是一个逐分量相乘的新向量,但是两个矩阵相乘得到的是通常矩阵相乘的结果。
image

我们还可以通过函数调用的方式实现常见的一些向量操作(例如,点乘和叉乘),以及各种逐分量执行的向量和矩阵操作。
流控制
GLSL的逻辑控制方式用的也是流行的if-else和switch语句。与C语言中的方式相同,else的分支是可选的,并且有多行语句时必须用到语句块:
image

switch语句的使用(从GLSL 1.30开始)与C语言中也是类似的,可以采用下面的方式:
image

GLSL的switch语句也支持“fall-through”形式:一个case语句如果没有使用break结尾,那么会继续执行下一个case的内容。每个case都需要执行一些语句,直到整个switch块结束(在右花括号之前)。此外,与C++当中不同的是,GLSL不允许在第一个case之前添加语句。如果所有的case条件都不符合,那么将会找到并执行default分支中的内容。
循环语句
GLSL支持C语言形式的for、while和do ... while循环。其中for循环可以在循环初始化条件中声明循环迭代变量。此时迭代变量的作用域只限于循环体内。
image
image

流控制语句
除了条件和循环之外,GLSL还支持一些别的控制语句。表2-7所示为其他可用的流控制语句。
image

discard语句只适用于片元着色器中。片元着色器的运行会在discard语句的位置上立即终止,不过这也取决于具体的硬件实现。
函数
我们可以使用函数调用来取代可能反复执行的通用代码。这样当然可以减少代码的总量,并且减少发生错误的机会。GLSL支持用户自定义函数,同时它也定义了一些内置函数,具体列表可以参见附录C。用户自定义函数可以在单个着色器对象中定义,然后在多个着色器程序中复用。
声明
函数声明语法与C语言非常类似,只是变量名需要添加访问修饰符:
image

函数名称可以是任何字符、数字和下划线字符的组合,但是不能使用数字、连续下划线或者gl_作为函数的开始。
返回值可以是任何内置的GLSL类型,或者用户定义的结构体和数组类型。返回值为数组时,必须显式地指定其大小。如果一个函数的返回值类型是void,那么它可以没有返回值。
函数的参数也可以是任何类型,包括数组(但是也必须设置数组的大小)。
在使用一个函数之前,必须声明它的原型或者直接给出函数体。GLSL的编译器与C++一致,必须在使用函数之前找到函数的声明,否则会产生错误。如果函数的定义和使用不在同一个着色器对象当中,那么必须声明一个函数原型。函数原型只是给出了函数的形式,但是并没有给出具体的实现内容。下面是一个简单的例子:
image

参数限制符
尽管GLSL中的函数可以在运行后修改和返回数据,但是它与“C”或者C++不同,并没有指针或者引用的概念。不过与之对应,此时函数的参数可以指定一个参数限制符,来表明它是否需要在函数运行时将数据拷贝到函数中,或者从函数中返回修改的数据。表2-8给出了GLSL中可用的参数限制符。
image

关键字in是可选的。如果一个变量没有包含任何访问修饰器,那么参数的声明会默认设置为使用in修饰符。但是,如果变量的值需要从函数中拷贝出来,那么我们就必须设置它为out(只能写出的变量)或者inout(可以读入也可以写出的变量)修饰符。如果我们写出到一个没有设置上述修饰符的变量上,那么会产生编译时错误。
此外,如果需要在编译时验证函数是否修改了某个输入变量,可以添加一个const in修饰符来阻止函数对变量进行写操作。如果不这么做,那么在函数中写入一个in类型的变量,相当于对变量的局部拷贝进行了修改,因此只在函数自身范围内产生作用。
2.3.4 计算的不变性
GLSL无法保证在不同的着色器中,两个完全相同的计算式会得到完全一样的结果。这一情形与CPU端应用程序进行计算时的问题相同,即不同的优化方式可能会导致结果非常细微的差异。这些细微的差异对于多通道的算法会产生问题,因为各个着色器阶段可能需要计算得到完全一致的结果。GLSL有两种方法来确保着色器之间的计算不变性,即invariant或者precise关键字。
这两种方法都需要在图形设备上完成计算过程,来确保同一表达式的结果可以保证重复性(不变性)。但是,对于宿主计算机和图形硬件各自的计算,这两种方法都无法保证结果是完全一致的。着色器编译时的常量表达式是由编译器的宿主计算机计算的,因此我们无法保证宿主机计算的结果与图形硬件计算的结果完全相同。例如:

image

在这个例子当中,无论对任何一个变量设置invariant或者precise限制符,结果都不会有任何改变,因为它们都只能影响到图形设备中的计算结果。
invariant限制符
invariant限制符可以设置任何着色器的输出变量。它可以确保如果两个着色器的输出变量使用了同样的表达式,并且表达式中的变量也是相同值,那么计算产生的结果也是相同的。
可以将一个内置的输出变量声明为invariant,也可以声明一个用户自定义的变量为invariant。例如:
image

你可能还记得,输出变量的作用是将一个着色器的数据从一个阶段传递到下一个。可以在着色器中用到某个变量或者内置变量之前的任何位置,对该变量设置关键字invariant。标准的做法是只使用invariant来声明这个变量,如上文中对gl_Position的设置。
在调试过程中,可能需要将着色器中的所有可变量都设置为invariance。可以通过顶点着色器的预编译命令pragma来完成这项工作。
image

全局都设置为invariance可以帮助我们解决调试问题;但是,这样对于着色器的性能也会有所影响。而为了保证不变性,通常也会导致GLSL编译器所执行的一些优化工作被迫停止。
precise限制符
precise限制符可以设置任何计算中的变量或者函数的返回值。它的名字有点望文生义,它的用途并不是增加数据精度,而是增加计算的可复用性。我们通常在细分着色器中用它来避免造成几何体形状的裂缝。第9章将大致介绍细分着色的内容,并且在其中对precise限制符进行更进一步的讲解和示例分析。
总体上说,如果必须保证某个表达式产生的结果是一致的,即使表达式中的数据发生了变化(但是在数学上并不影响结果)也是如此,那么此时我们应该使用precise而非invariant。举例来说,下面的表达式中,即使a和b的值发生了交换,得到的结果也是不变的。此外即使c和d的值发生了交换,或者a和c同时与b和d发生了交换等,都应该得到同样的计算结果。
image

precise限制符可以设置内置变量、用户变量,或者函数的返回值。
image

在着色器中,关键字precise可以在使用某个变量之前的任何位置上设置这个变量,并且可以修改之前已经声明过的变量。
编译器使用precise的一个实际影响是,类似上面的表达式不能再使用两种不同的乘法命令来同时参与计算。例如,第一次相乘使用普通的乘法,而第二次相乘使用混合乘加运算(fused multiply-and-add,fma)。这是因为这两个命令对于同一组值的计算结果可能会存在微小的差异。而这种差异是precise所不允许的,因此编译器会直接阻止你在代码中这样做。由于混合乘加运算对于性能的提升非常重要,因此不可能完全禁止用户使用它们。所以GLSL提供了一个内置的函数fma(),让用户可以直接使用这个函数代替原先的操作。
image

当然,如果不需要考虑交换a和c的值,那么没有必要使用这种写法,因为那个时候也没有必要使用precise了。
2.3.5 着色器的预处理器
编译一个GLSL着色器的第一步是解析预处理器。这一点与C语言中的预处理器类似,并且GLSL同样提供了一系列命令来有条件地生成编译代码块,或者定义数值。不过,与C语言的预处理器不同的是,GLSL中没有文件包含的命令(#include)。
预处理器命令
image
image

宏定义
GLSL预处理器可以采取与C语言预处理器类似的宏定义方式,不过它不支持字符串替换以及预编译连接符。宏可以定义为单一的值,例如:
image

或者带有参数,例如:
image

此外,GLSL还提供了一些预先定义好的宏,用于记录一些诊断信息(可以通过#error命令输出),如表2-10所示。
image

此外,也可以通过#undef命令来取消之前定义过的宏(GLSL内置的宏除外)。例如:
image

预处理器中的条件分支
GLSL的预处理器与C语言的预处理器相同,都可以根据宏定义以及整型常数的条件来判断进入不同的分支,包含不同的代码段。
宏定义可以通过两种方式来参与条件表达式,第一种方式是使用#ifdef命令:
image

或者在#if和#elif命令中使用操作符来进行判断:
image

2.3.6 编译器的控制
`#pragma命令可以向编译器传递附加信息,并在着色器代码编译时设置一些额外属性。
编译器优化选项
优化选项用于启用或者禁用着色器的优化,它会直接影响该命令所在的着色器源代码。可以通过下面的命令分别启用或者禁用优化选项:

image

这类选项必须在函数定义的代码块之外设置。一般默认所有着色器都开启了优化选项。
编译器调试选项
调试选项可以启用或者禁用着色器的额外诊断信息输出。可以通过下面的命令分别启用或者禁用调试选项:

image

与优化选项一样,这些选项只在函数定义的代码块之外设置,而默认情况下,所有着色器都会禁用调试选项。
2.3.7 全局着色器编译选项
另一个可用的#pragma命令选项就是STDGL。这个选项目前用于启用所有输出变量值的不变性检查。
着色器的扩展功能处理
GLSL与OpenGL类似,都可以通过扩展的方式来增加功能。设备生产商也可以在自己的OpenGL实现中加入特殊的扩展,因此很有必要对着色器中可能用到的扩展功能进行编译级别的控制。
GLSL预处理器提供了#extension命令,用于提示着色器的编译器在编译时如何处理可用的扩展内容。对于任何扩展,或者全部扩展,我们都可以在编译器编译过程中设置它们的处理方式。

image

从而直接影响所有扩展的行为。
< directive>可用的选项如表2-11所示。
image

相关文章
|
算法 Android开发
Android OpenGL ES(二)----平滑着色
Android OpenGL ES(二)----平滑着色
99 0
Android OpenGL ES(二)----平滑着色
|
缓存 图形学
《OpenGL编程指南(原书第9版)》——导读
OpenGL图形系统是图形硬件的一种软件接口(GL表示Graphics Library,即图形库)。它使得用户可以创建交互式的程序以产生运动的三维对象的颜色图像。通过OpenGL,我们可以使用计算机图形学技术产生逼真的图像,或者通过一些虚构的方式产生虚拟的图像。
3024 0
|
存储 缓存 API
三、OpenGL ES GLSL语言 & 自定义着色器常用API
OpenGL ES GLSL语言 & 自定义着色器常用API
320 0
三、OpenGL ES GLSL语言 & 自定义着色器常用API
|
存储 缓存 并行计算
使用计算着色器(Compute Shader)模拟粒子效果【OpenGL】【GLSL】
使用计算着色器(Compute Shader)模拟粒子效果【OpenGL】【GLSL】
913 0
使用计算着色器(Compute Shader)模拟粒子效果【OpenGL】【GLSL】
|
存储 缓存 索引
《OpenGL编程指南(原书第9版)》——3.4 OpenGL的绘制命令
本节书摘来自华章计算机《OpenGL编程指南(原书第9版)》一书中的第3章,第3.4节,作者:(美)约翰·克赛尼希(John Kessenich)著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2498 0
|
存储 缓存 数据格式
《OpenGL编程指南(原书第9版)》——3.3 顶点规范
本节书摘来自华章计算机《OpenGL编程指南(原书第9版)》一书中的第3章,第3.3节,作者:(美)约翰·克赛尼希(John Kessenich)著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1484 0
|
存储 缓存
《OpenGL编程指南(原书第9版)》——3.2 OpenGL缓存数据
本节书摘来自华章计算机《OpenGL编程指南(原书第9版)》一书中的第3章,第3.1节,作者:(美)约翰·克赛尼希(John Kessenich)著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1799 0
OpenGL ES 中着色器变量 sampler2D 并不是在宿主语言中指定的数字 0...
OpenGL ES 中着色器变量 sampler2D 并不是在宿主语言中指定的数字 0... 太阳火神的美丽人生 (http://blog.csdn.net/opengl_es) 本文遵循“署名-非商业用途-保持一致”创作公用协议 转载请保留此句:太阳火神的美丽人生 -  本博客专注于 敏捷开发及移动和物联设备研究:iOS、Android、Html5、Arduino、pcDuino,否则,出自本博客的文章拒绝转载或再转载,谢谢合作。
1066 0
|
缓存 Windows
NeHe的OpenGL教程3(Bang翻译Delphi版)-如何给图形着色
NeHe的OpenGL教程3(Bang翻译Delphi版)-如何给图形着色 作为第二课的扩展,我将教你如何使用颜色。你将理解两种着色模式,在左图中,三角形用的是光滑着色,四边形用的是平面着色。
915 0
|
7月前
|
XML 小程序 Java
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
132 0