opengl shader 入门 超详细

来源:互联网 发布:仓鼠翻译软件 编辑:程序博客网 时间:2024/06/14 06:11

http://bbs.gameres.com/upload/sf_20061018193133.pdf



第三章:语言的定义 

John Kessenich
在这章里,我们将介绍 OpenGL Shading Language 的所有特性。首先,我们通过一对简单的
vertex shader  和 fragment shader 的例子来展示它们的基本结构和接口,然后在依次介绍语言的
各个方面。
OpenGL Shading Language 的语法来自 C 语言家族(译者注:包括 C/C++,JAVA 等)。记号、
标识符、分号、花括号的嵌套、流程控制以及很多的关键字都和 C 语言非常像。// ...和/* ... */  两
种风格的注释都可以用。但是它和 C 语言之间还是有很多不同。随后将讨论这些重要的不同之
处。
每个 shader 的例子可能以一个文件的形式存在,也可能存在在屏幕上(译者注:就是以字
符串的形式出现在普通的源代码中)。但是,如同第 7 章描述的,OpenGL API 是以字符串的形
式传递 shader 的,而不是以文件的形式。因为 OpenGL 不认为 shader 一定是基于文件的。[as 
OpenGL does not consider shaders file based.] 
3.1 一对 Shader 的例子
一个程序通常包含两个 shader,一个 vertex shader 和一个 fragment shader。每种 shader
同时可以有多个。但是所有的 vertex Shader 和所有的 fragment shader 只能有一个 main 函数。
通常,每种类型的 shader 只有一个的话会更加简单一些。 
下面是一对简单的 vertex shader 和 fragment shader,他们能用平滑的颜色来表示一个
表面的温度。温度的范围和颜色可以通过参数来指定。首先我们来看 vertex shader,每个顶
点都会让它执行一次。 
//用 uniform 指定的变量,每个图元可以有不同的值(changed per primitive) 
uniform float CoolestTemp; 
uniform float TempRange; 
//用 attribute 指定的变量各个顶点可以有不同的值(changed per vertex)
attribute float VertexTemp; 
//用 Varying 指定的变量用来在 vertex shader 和 fragment shader 之间通信 
varying float Temperature; 
void main() 

   //逐片段的插值计算温度, 
    // 范围 [0.0, 1.0]
    Temperature = (VertexTemp - CoolestTemp) /  TempRange; 
/* 在应用程序中用 glVertex()指定的顶点位置,在顶点着色器中可以用内置
的变量 gl_Vertex 来取得。用这个值(gl_Vertex)和当前的模型视图变换
矩阵来告诉光栅化器这个顶点在哪里(即在屏幕上的位置). 
*/
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; 

这就是 vertex shader.图元将按照预先给定的步骤(也就是 shader 代码)处理。给光栅化器
提供足够的信息来创建一个片段。光栅化器对逐顶点计算出来的温度进行插值计算,为每个片
段产生一个温度值。接着,每个片段的信息被输入到如下的 fragment shader 里去执行: 
//用 uniform 指定的变量,每个图元可以有不同的值(changed per primitive) 
//被声明成一个 vector 类型的向量,它有三个浮点型的成员 
uniform vec3 CoolestColor; 
uniform vec3 HottestColor;
// Temperature 是对 vertex shader 传过来的值进行逐片段的插值而计算出来的
varying float Temperature; 
void main() 

    //用内置的 mix()函数来得到一个处于最冷和最热温度之间的颜色值。 
    vec3 color = mix(CoolestColor, HottestColor, Temperature);
// 给 color 增加一个值为 1.0 的 alpha 分量,生成一个包含 4 个浮点数成员的向量。并
把它设置成当当前的片段颜色
    gl_FragColor = vec4(color, 1.0); 
}
两个 shader 都可以通过声明一个 uniform 的变量来接受应用程序传递过来的自定义的参
数。Vertex shader 可以通过 attribute 属性的变量来得到和每个顶点关联的信息。从 vertex 
shader 传递到 fragment shader 的信息则通过 varying 属性指定的变量,varying 属性的变量
在一对 vertex shader 和 fragment shader 之间的定义必须匹配(注:即在 vertex shader 里
定义了一个,则在 fragment shader 里也必须有一个)。处在 vertex shader 和 fragment shader
之间的固定功能管道将对 varying 变量进行插值。当一个 fragment shader 读取一个 varying
变量的时候,它得到的值是已经插值过的。 
Shader 通过读取内置的变量来和 OpenGL fixed functionality pipeline 进行交互。这些
内置的变量都有一个统一的前缀"gl_"。在前面的例子里。把值写到 gl_Postion 里是告诉 OpenGL 
pipeline 这个顶点变换后的位置。把值写到 gl_FragColor 里是告诉 OpenGL pipeline 这个片
元是什么颜色。 
处理一个图元会多次执行前面的 shader。Vertex shader 是每个顶点执行一次,fragment
则是每个片段执行一次。很多这样的相同的 shader 执行过程可以是并行的。这些执行过程没有
直接的联系和顺序。顶点和顶点之间,以及片段和片段之间是无法进行通信的。 3.2  数据类型
我们已经在前面的例子里使用了浮点型的 vector。还有其它很多可用的内置类型可以使图
形处理变的更加的容易。布尔(Boolean),整数(integer),矩阵(matrices),其他的数据类型的
向量(vector),结构体(structure),数组(array)等都包含在里面。它们将在随后的小节里讨
论。字符串(string)和字符(character)类型并没有出现,因为在处理顶点和片元的时候用处非
常少。 
3.2.1  标量
以下是标量的类型 
float 声明单个的浮点数 
int 声明单个的整数 
bool 声明单个的 boolean 类型的变量。 
类似于 C/C++,这些类型用来声明变量。 
float f; 
float g, h = 2.4; 
int NumTextures = 4; 
bool skipProcessing; 
和原生的 C 语言不同。因为没有默认的类型,你必须提供类型名称。变量可以随用随定义,而
不是只能在花括号({})后边定义.在这点和 C++是一样的。  
浮点数的声明文法(也就是表示方式)也是和 C 一样的,只是不需要用后缀(如 C 里的 f )来
表示精度,因为 GLSL 里只有一种浮点数的精度 
3.14159 
3. 
0.2 
.609 
1.5e10 
0.4E-4 
etc. 
通常,浮点数的操作和运算规则和 C 语言里的是一样的 
整型数和 C 语言不同,There is no requirement that they appear to be backed in hardware 
by a fixed-width integer register. 从而,当一个定宽的整数在运算过程中发生上溢和下溢
的时候,它的回绕(wrapping)行为是没有定义的。位操作,比如说左移(<<)和与(&)操作也不支
持. 那整数都有些什么特性呢?我们可以保证整数有至少 16-bits 的精度,它可以是真的,负的,
也可以是 0。在给定的范围内进行的数值运算都可以得到期望的结果。主要到精度是 16 位,再
加上一个符号位,整数可以表达的范围为[-65535, 65535],或者更大(因为精度至少是 16 位)
和 C 语言里的整数一样,GLSL 的整数描述文法可以是十进制,八进制,或者十六进制。 
42    //十进制整数 
052   //八进制整数 
0x2A  //十六进制整数 
同样的,因为只有一种整数类型,所以也不需要后最来指定整数的精度,整数在表达一个结构
或者一个数组的大小以及循环的计数器的时候非常有用。在 Shader 里,和图形有关的量,比如
颜色、位置等最好用浮点类型来表达。 
Boolean 类型就是 C++里的 bool。它只能是两个值里的一个:true 或者 false.true 和 false 在
就是 Boolean 类型的两种常量表示。和它相关的操作,比如 小于(<),逻辑与(&&)等,都会产生
一个 Boolean 类型的值。流程控制结构 if-else 只能接受一个 Boolean 类型的表达式.有了这点
保证,OpenGL Shading Language 比 C++更严格. 
3.2.2  向量 (Vectors) 
浮点数,整数以及布尔类型的向量是内建的数据类型,它们可以有两个,三个或者四个分量。
名字如下表: 
vec2 有 2 个浮点型分量的向量(2D 向量) 
vec3 有 3 个浮点型分量的向量(3D 向量) 
vec4 有 4 个浮点型分量的向量(4D 向量) 
ivec2 有 2 个整型分量的向量 
ivec3 有 3 个整型分量的向量 
ivec4 有 4 个整型分量的向量 
bvec2 有 2 个布尔型分量的向量 
bvec3 有 3 个布尔型分量的向量 
bvec4 有 4 个布尔型分量的向量 
内建的向量类型非常有用,用它来存储和操作颜色、位置、纹理坐标非常的方便。内建的变量
和函数都用到了这种类型,它们还支持一些特定的操作。Finally, hardware is likely to have 
vector-processing capabilities that mirror vector expressions in shaders.
注意到语言并不区分颜色向量和位置向量以及浮点向量的其他用途。这就是从语言的角度的向
量(并不以功能区分)。 我们可以通过访问字段(field)的形式来访问向量的分量(类似与结构体 structure),也可以
以数组的形式访问。举个例子,如果一个 vec3 型的 postion 变量,我们可以把它当成
vector(x,y,z),position.x 就是第一个成员。 
总之,以下的都是用来访问一个向量的分量的: 
x, y, z, w  把一个向量当作位置来看 
r, g, b, a  把一个向量当作颜色来看 
s, t, p, q  把一个向量当作纹理坐标来看 
没有显式的方法区分一个向量是用来表示颜色、位置还是一个纹理坐标。分量的选取名字
(selection names)仅仅是为了让 Shader 有更好的可读性。编译的时候,只是检查向量的大
小是不是足够存储指定的分量(一个 vecector3 是没有.a 和.w 分量的)。而且,如果一次选择
了多个分量(swizzling,在第 3.7.2 节里讨论)的情况也是相同的(也是仅仅检查向量是不是足
够的大). 
向量也可以通过一个以 0 开始的数字下标(index)来索引分量,即向量也可以被看成一个数组。
Position[2]返回的是 position 的第三个分量。下标也可以一个变量,这就允许我们通过循环
的方式来检索一个向量的所有分量。向量的乘法有专门的含义,向量和矩阵的乘表示一个线性
变换。Swizzling,索引,以及其他的操作将在 3.7 节里详细介绍。 
3.2.3  矩阵
内建类型里还有浮点类型的矩阵,有 2 x 2 , 3 x 3, 和 4 x 4 的矩阵。 
mat2 2 x 2 浮点类型的矩阵 
mat3 3 x 3 浮点类型的矩阵 
mat4 4 x 4 浮点类型的矩阵 
矩阵用来存储线性变换非常有用,语义上我们把它当成矩阵,一个向量和一个矩阵相乘时,等
效于在向量上施加了一个对应的线性变换。相应的,在 OpenGL 里,矩阵是按列优先
(Column-Major)的方式组织的。 
你可以把一个矩阵当作一个列向量的数组来使用,如果 transform 是一个 mat4 型变量,
transform[2]是 transform 的第三个列向量,transform[2]的类型是一个 vec4,Column 0 是
第一个列向量(这点又是 C 语言的习惯),因为 transformp[2]是一个向量,你也可以把一个向
量当成数组来看,transform[3][1]就是第四个列向量的第二个分量。所以我们可以把一个矩阵
当作一个二维数组来用。记住第一个下标表示的是列,而不是行,第二个下标指示的才是行。 3.2.4 取样器
纹理数据的检索需要一些信息来明确是检索什么纹理或者哪一个纹理单元,OpenGL Shading 
Language 并不真正关心纹理单元的实现和其它形式的纹理检索硬件的细节。因此,它提供了一
个不透明的操作来封装纹理的操作方式,这个操作被成为取样器(SAMPLERS)。取样器的类型
有以下几种: 
sampler1D 访问一个 1D 的纹理 
sampler2D 访问一个 2D 的纹理 
sampler3D 访问一个 3D 的纹理 
samplerCube 访问一个 CubeMap 的纹理 
sampler1DShadow 访问一个 1D 深度纹理,带比较操作 
sampler2DShadow 访问一个 2D 深度纹理,带比较操作 
当应用程序初始化了一个取样器的时候,OpenGL 的实现(译注 OpenGL 是个规范,需要各个厂
家去实现)将查询一个纹理所需要的信息都保存到取样器里。Shaders 本身并不能初始化一个
取样器,也不能把它(sampler)传递个用户或者是内建的函数。作为一个参数,取样器不能被
修改,所以 shader 不能改变一个取样器的值。 
举个例子,一个取样器可以以这样的方式来声明: 
uniform sampler2D Grass; 
(Uniform限定符将在3.5节详细介绍.) 
这个变量可以被传递给相应的纹理检索函数来访问纹理: 
vec4 color = texture2D(Grass, coord); 
coord 是一个 vec2,保存了 2 维的、草的纹理的坐标。纹理检索的结果是一个颜色值。OpenGL API 
和编译器会检查 Grass 是不是真的装入了一个二维纹理以及 Grass 是不是传递给 2 维的纹理检
索函数(texture2D)。 
Shaders 并不操作取样器的值。举个例子,Grass+1 是非法的。如果一个 shader 想要组合多个
纹理,可以使用下面介绍的取样器数组的方法: 
const int NumTextures = 4; 
uniform sampler2D textures[NumTextures]; 
These can be processed in a loop: 
for (int i = 0; i < NumTextures; ++i) 
    ... = texture2D(textures[i], ...); Grass+1 这样的惯用方法相应的变成了类似 textures[GrassIndex + 1],这是一种操作使用哪一
个纹理取样器的合法途径。 
3.2.5  结构体
OpenGL Shading Language 提供了一种类似与 C 语言的自定义结构体。举个例子, 
struct light 

    vec3 position; 
    vec3 color; 
}; 
和 C++一样,结构体的名字就是自定义类型的名字,不要 typedef。事实上,typedef 也是个保
留字,但是目前还不需要它,一个前面例子中定义的 light 型变量声明如下: 
light ceilingLight; 
结构体的其他方面和 C 语言也一样,可以内嵌(也称局部结构体,C++里有局部类)和嵌套定义。
内嵌结构体的类型名字和结构体的声明的语句块(the structure they are declared in)有
相同的作用域。最后一点,和 C 语言一样,每个层次上的结构体里成员都有它们自己的名字空
间(这个名字空间就是这个结构体)。 
不支持 Bit-Fields,bit-fields 是用来声明一个有指定 bit 位数的整数的方法。 
目前,结构体是唯一的自定义数据类型,关键字 union ,enum 和 class 都是保留字,一满足将
来的需求。 
3.2.6 Arrays 
可以创建一个任何数据类型的数组, 
vec4 points[10]; 
创建了一个有 10 个 vec4 的数组,下标从 0 开始。GLSL 里没有指针的概念,声明数组唯一的方
法就是用一对方括号。数组不一定需要声明一个大小,如下的声明 
vec4 points[]; 
是允许的,直到以下的两种情况成立: 
1.在使用数组前,我们用一个给定的大小重新声明了这个数组,比如: 
vec4 points[];    // points 是一个大小未知的数组。 
vec4 points[10];  // points 现在是有 10 个元素的数组。 
在这以后,就不能重新声明这个变量了: vec4 points[];    // points 是一个大小未知的数组 
vec4 points[10];  // points 现在是有 10 个元素的数组。 
vec4 points[20];  // 非法的声明 
vec4 points[];    // 这也是非法的声明 
2.所有操作这个数组的下标在编译期均为常量,在这种情况下,编译器会让数组足够大,
以满足用到的最大的下标。比如:  
vec4 points[];         //points 一个大小未知的数组。 
points[2] = vec4(1.0); //points 现在是有 3 个元素的数组 
points[7] = vec4(2.0); //points 现在是有 8 个元素的数组 
这种情况下,在运行时(runtime),数组的大小只有一个:它是由编辑器检测到的最大下标决定
的。 
这种特性在操作内建的纹理坐标数组的时候非常有用,在 GLSL 内部,纹理坐标数组的声明如
下: 
varying vec4 gl_TexCoord[]; 
如果程序(Shader 程序)在编译时只用到了下标 0 和 1,那么这个数组就隐式的的等效于
g_TexCoord[2]。如果需要用一个变量来索引数组,那么 shader 必须事先明确的声明数组的大
小。当然,尽量的让数组的大小最小化也是非常重要的,尤其是对 varying 型的变量,因为它
们的资源是受到硬件的限制的。 
多个 Shader 共享一个相同的数组的时候,它们可以各自声明为不同的大小,连接器(linker)
在连接的时候会自动取他们中最大的那个。 
3.2.7 Void 
Void 型提供了一种声明没有返回值的函数的方法,比如,如果 main 函数没有返回类型,它必
须这样声明: 
void main() 

    ... 

除了声明没有返回值的函数以外,void 类型没有其他的用途。 
3.2.8 声明和作用域
变量的声明和 C++非常的像,可以随用随定义,以及和 C++一样的作用域规则,比如: 
float f; 
f = 3.0; 
vec4 u, v; for (int i = 0; i < 10; ++i) 
    v = f * u + v; 
一个在for语句里定义的变量,作用域只到循环语句的结束。可是,变量可能并不在if语句里定
义,simplifying implementation of scoping across the else sub-statement, with little 
practical cost. 
如同 C 语言,变量名是大小写敏感的,必须以字母或者下划线(_)开始,并且只能由字母、数
字、和下划线(_)。用户定义的变量不能以”gl_”开始,因为这是给 OpenGL 保留的(如所有
的内建变量都是由 gl_打头的)。包含了连续两个下划线(__)的的变量也作为保留字。 
3.2.9 类型匹配和提高(Promotion)
OpenGL Shading Language 是要求严格匹配类型的。通常,赋值的类型必须匹配,传递给函数
的实参必须和函数形参的类型一致,传递给运算符的类型也必须符合运算符的要求。类型不会
自动提升(promotion)为另外一种类型。这有时会让 Shader 有额外的限制。但是,这让语言
更加简单,避免了一些常见混淆。例如,在调用一个函数的时候,不会因为重载问题而不知道
选择哪个函数。 
3.3 Initializers and Constructors 
Shader 的变量在声明的时候可能被初始化,如同 C 语言一样,以下的例子里,b 在声明的时候
初始化,而 a,c 没有: 
float a, b = 3.0, c; 
用 constant 限定符修饰的变量必须被初始化。 
const int Size = 4;  // 必须初始化 
Attribute, uniform, 和 varying 变量在声明的时候不能初始化 
attribute float Temperature;  // 不允许初始化, 
                              // vertex API 会正确设置它 
uniform int Size;             //不允许初始化, 
                              // uniform 设置 API 会设置它 
varying float density;        //不允许初始化, vertex 
                              // shader 必须在程序里设置这个变量 
初始化集合类型(vector,matrix 等)时,不管是在声明时还是其他时刻,构造器(CONSTRUCTORS)
将被使用。这里没有和 C 语言里的花括号"{…}"类似的初始化方法,只有构造器。在词义上,
构造器看上去像个函数调用,只是原本写函数名的位置是类型的名字(和 C++里的构造函数一
样),例如:把一个 vec4 初始化成(1.0, 2.0, 3.0, 4.0)的代码如下: vec4 v = vec4(1.0, 2.0, 3.0, 4.0); 
另外,无论是在初始化或者在其他地方,构造器在词义上都是一样的。 
vec4 v; 
v = vec4(1.0, 2.0, 3.0, 4.0); 
和结构体一样,所有的内建类型都有构造器(除了取样器),例如: 
vec4 v = vec4(1.0, 2.0, 3.0, 4.0); 
ivec2 c = ivec2(3, 4); 
vec3 color = vec3(0.2, 0.5, 0.8); 
mat2 m = mat2(1.0, 2.0, 3.0, 4.0); 
struct light 

    vec4 position; 
    struct lightColor 
    { 
        vec3 color; 
        float intensity; 
    } 
} light1 = light(v, lightColor(color, 0.9)); 
矩阵的各个分量一列优先的方式填写,前面的例子里的变量 m 就一个矩阵。 
到目前为止,我们介绍的构造器为变量的每个分量都赋了一个值,vector 的内建构造器可以只
接受一个参数,其它分量的值将从这个值复制。 
vec3 v = vec3(0.6);和 vec3 v = vec3(0.6, 0.6, 0.6)是等效的。 
这仅仅是对向量类型而言的,结构体在构造的时候必须为每个成员都指定一个值。矩阵构造器
也可有只接受一个参数的形式,但是在这种情况下,只初始化矩阵的对角线,其他的分量都被
初始化成了 0。 
mat2 m = mat2(1.0);  // 初始化了一个 2x2 的单位矩阵 
等效于 
mat2 m = mat2(1.0, 0.0, 0.0, 1.0);    //初始化了一个 2x2 的单位矩阵 
构造器也可以使用向量和矩阵做参数。唯一的规则就是参数必须有足够的分量来初始化变量的
所有成员。 
vec4 v = vec4(1.0); 
vec2 u = vec2(v);  //  v 的前两个分量初始化了 u 
mat2 m = mat2(v); vec2 t = vec2(1.0, 2.0, 3.0);  // 这是允许的,3.0 简单的被忽略了。 
矩阵的各个分量以列优先的方式被读出来、以列优先的方式被填写。多余的分量或者参数被简
单的忽略掉。这在想收缩(shrinking)一个值的是非常有用,比如把一个颜色的 alpha 分量或者
位置的 w 分量消除掉。 
3.4 Type Conversions 类型转化
显式的类型转化通过构造器完成,比如: 
float f = 2.3; 
bool b = bool(f); 
将把 b 设置为 true,这点在流程控制上非常有用,类似 if,which 需要一个布尔类型的值,布尔
类型的构造器把一个非 0 的值转化为 true,把值为 0 的转化为 false. 
OpenGL Shading Language 不提供类似于 C++的类型转化,C++里的类型转化经常会引擎混淆:是
转化为另外一种类型呢?还是重新解释为另外一种类型。事实上,GLSL 里没有把一种类型重新
解释为另外一种类型的方法,这里没有指针,没有 union,没有隐式的类型转化,也没有
reinterpret cast。我们只能使用构造器来代替类型转化,传给构造器的参数将被转化为构造
出来的类型。因此,以下的方式是允许的: 
float f = float(3);  // 把整型的 3 转化为浮点型的 3.0 
float g = float(b);  // 把布尔类型的 b 转化为浮点型的 g 
vec4 v = vec4(2);    // 向量 v 的所有分量都被设置成 2.0。 
当转化一个布尔类型的值的时候,true 被转化 1 或者 1.0,false 被转化为 0(0.0 浮点的时候,
注意 0 是整数,而 0.0 是浮点数,前面的 1 和 1.0 也相同)。 
3.5 限定符和 Shader 的接口
限定符可以修饰变量和函数的形式参数。可以修饰函数形式参数的限定符(const ,in,out,和
inout)将在第 3.6.2 节讨论,这一节将针对其他的限定符,他们的大部分组成了 shader 的接
口。以下是除了函数形参以外的所有限定符的列表: 
attribute 经常改变的信息,从应用程序传递到 vertex shader 
uniform 不经常改变的信息,vertex 和 fragment shader 都有 
varying 从 vertex shader 到 fragment shader 传递一个需要插值的信息 
const 和 C 语言一样,声明一个只读的,编译时刻的常量 
从一个 shader 里传入和传出数据在典型的编程环境里是很不一样的。从一个 shader 传入和传
出数据是通过读写内建变量和用户定义的 attribute,uniform,和 varying 变量来实现的。最常
见的变量在本章开头的例子里介绍过了。它们分别是是 gl_Position,用来输出顶点位置的齐次坐标,以及 gl_FragColor,Fragment Shader 用它来输出片段的颜色。所有的内建变量的列
表在第四章里有提供,attribute,uniform ,和 varying 变量在本长开头的例子里有简单的介绍,
我们使用它们来给 shader 传递信息和读取信息。在这节里,我们将逐个的讨论。 
变量的限定符,attribute,uniform,和 varying 必须声明为全局,这是非常明显的,因为它们都
需要在 shader 以外使用。针对单个的程序(一个程序可能由多个 shader 组成),它们都共享
一个相同的名字空间 
限定符通常在变量的类型名前指定,因为没有默认的类型,由限定符修饰的变量一定会有一个
类型。 
attribute float Temperature; 
const int NumLights = 3; 
uniform vec4 LightPosition[NumLights]; 
varying float LightIntensity; 
3.5.1 Attribute  限定符
Attribute 用来指示一个变量或者属性的数据是由应用程序提供给 shader 的,这些数据需要经
常变动(在应用程序端),它们最多每个顶点变化一次,这种变化由程序直接或者间接的产生,
有内建的属性,如 gl_Vertex 和 gl_Normal,用来读取 OpenGL 的传统状态,以及其他的自定义
属性,我们可以自己命名它们的名字。 
Attribute 只能是浮点类型的标量,浮点类型的向量,和浮点类型的矩阵使用。没有整数,布
尔,结构体,或者 attribute(注:以后 attribute 修饰的变量简称为 attribute。Uniform 等
限定符也类似)数组。这是为了让 OpenGL 系统改变 attribute 的时候更加的有效,attribute
在 shader 里不能被修改。 
限定符也不能在 fragment shader 里使用。 
3.5.2 Uniform  限定符
Uniform 限定符修饰的变量(uniforms),类似于 attribute,它也是在 shader 的外部被设置的,
主要是面向那些不是经常需要改变的数据。Uniforms 变量最多每个图元改变一次(译者注:
attribute 是可以由 glVertex、glColor 等函数设置的,所以可以每个顶点改变一次。而设置
uniform 的函数不能在 glBegin/glEnd 之间调用,所以只能最多每个图元改变一次)。Uniform
支持所有的数据类型,以及所有的数据类型的数组。如果一个程序包含多个 vertex / fragment 
shader,他们共享一个相同的全局的 uniform 变量的命名空间。因此,如果一个 uniform 变量
在 vertex program 和 fragment program 里有相同的名字的话,它们是同一个变量。 
Uinform 在一个 shader 里是不能被写如的。这是因为多个处理器可能共享一个相同的资源来保
存一个 Uniform,如果一个 uniform 被改变了,就打破了语义上 unifrom 的”一致性”。(This 
is sensible because an array of processors may be sharing the same resources to hold 
uniforms and other language semantics break down if uniforms could be modified.)回忆一下,除非把一个取样器(sampler)当作一个函数的参数,否则在声明一个取样器的时候
必须使用 uniform 限定符。这是因为取样器是不透明的,把它们声明成一个 uniform 的变量允
许系统用相应的纹理很纹理单元把取样器初始化好。 
3.5.3 Varying  限定符
Varying 限定符修饰的变量(简称 Varying),是在 Vertex Shader 和 fragment Shader 之间
通信的唯一途径。类似的变量在 fragment shader 和 vertex shader 之间建立了接口。主要意
图是为了针对绘制图元时的一些特殊属性,每个顶点可能又不同的值,这些值在图元光栅化的
时候需要进行插值。Vertex shader 把这些值写入到 varying 变量中去,当 fragment shader
读取这些变量的时候,得到的是经过插值后的数据。如果一个数据在一个图元中都是相同的(图
元所有的 fragment中都相同),那么 vertex shader就没有必要把这个值传递给 fragment shader
进行通信,而只需要直接把这个值通过 uniform 变量传递给 fragment shader 就可以了。 
使用 varying 的例外是当一个值在程序中需要经常改变的时候,无论是每个三角形或者顶点之
类更小的集合内经常变化,这样的值可能通过一个 attribute 变量传递给 vertex shader。然
后通过 varying 变量继续传递下去会比直接使用 uniform 变量两的更加有效率。 
Varying 变量的自动插值是需要通过透视矫正的。这对无论是什么类型的数据都是必须的。否
则这些数据在曲面细分的边缘会变的不平滑。but its derivative would not be. 
一个 varying 变量通常在 vertex shader 中被写入,然后在一个 fragment shader 里被读取。
Vertex shader 也许会读一个 varying 变量,取回刚刚写入的数据。如果去读一个没有被写入
数据的 varying 变量,这样返回值是没有定义的。 
3.5.4 Constant  限定符
一个变量如果用 constant 修饰的话(除了函数的形参以外),在编译期间是一个常量,在声明
这个变量的 shader 以外是不可见的。 我们可以声明非标量的常量。 
const int numIterations = 10; 
const float pi = 3.14159; 
const vec2 v = vec2(1.0, 2.0); 
const vec3 u = vec3(v, pi); 
const struct light 

    vec3 position; 
    vec3 color; 
} fixedLight = light(vec3(1.0, 0.5, 0.5), vec3(0.8, 0.8, 0.5));
前面定义的变量都是编译期常量,编译器将处理这些变量,使用处理器能支持的精度来表示它
们。在运行期间,不需要为 const 型变量分配资源。 3.5.5 Absent 限定符
如果在声明的时候没有指定变量的限定符,变量将在这个 shader 里可读可写。无限定符
(Nonqualified)变量在相同类型(vertex shader 和 fragment shader)的、连接到一个 program
的 shader 里是可以共享的。但是对于 Nonqualified  变量,vertex shader 和 fragment shader
有不同的全局名字空间。因此,自定义的 nonqualified 变量在 program 以外是不可见的。这
种变量在 program 之外可见的特权被保留给 uniform,attribute 和用来表示 Opengl 状态的内
建变量。 
Nonqualified 变量的生命周期是 shader 的一个执行期。也没有类似于 C 语言的 static 修饰
符,可以让这个变量在这个 shader 执行完到下一次执行时,仍然能保留着上次的被设定的值。
如果这样的话,将会让并行处理更加困难,当有几个执行过程并发的时候,它们就会使用一个
相同的存储器。通常,可写的变量必须对每一个实例(这里是每个 shader 的一个执行过程)都
唯一。因此这样的变量是不能在两个执行过程共享。 
因为 vertex shader 和 fragment shader 对 Nonqualified 变量,有不同的全局名字空间,因
此我们不能使用 nonqualified 变量来进行两种 shader 之间的通信。只读的变量可以使用
uniform 在两种 shader 间进行通信。如果一个变量需要在 vertex shader 写入,在 fragment 
shader 端读出的话,只能通过 varying 机制。 
3.6 流程控制
流程控制和 C++非常类似。一个 shader 的入口是 main 函数,一个包含了 vertex shader 和
fragment shader 的程序有两个 main 函数。一个作为 vertex shader 的入口,一个作为 fragment 
shader 的入口。在执行 main 函数前,所有的全局变量的声明都将被执行。 
可以用 for ,while 和 do-while 来实现。变量可以在 for 和 while 语句中定义,它们的生命
周期在子语句末尾结束。break 关键字的作用和 C 语言里一样。  
类似于 C++,选择可以通过 if / if-else 来实现。一个例外就是在 if 语句里不能声明变量。用
(?:)操作符号来进行选取也是可以的,比较严格的是第二和第三个操作数的类型必须相同。 
If 和 while 语句里的表达式,或者用来结束语句的表达式,必须是个 boolean 类型的标量。和
C 语言里一样,逻辑与操作符(&&)左边的表达式为 false 的情况下,右边的表达式不会被计
算。逻辑或操作符(&&)左边的表达式为 true 的情况下,右边的表达式不会被计算。类似的,
在选择操作符(:?)中,只有一个表达式会被计算。逻辑异或(^^)操作也支持,操作符两端
的表达式都需要计算。 
一个特殊的分支:discard,能阻止一个片段被写入到帧缓存里,当流程控制到达 discard 关键
字的时候,正在处理的片段被标记为 discard,GL 的实现可能继续也可能终止这个 shader 的执
行。但是都保证这个片段不会影响到帧缓存。 3.6.1 函数
函数调用的操作非常类似于 C++,函数的名字可以被重载,用函数的参数来区分,但是不能单独
以返回值来区分重载函数。在函数被调用前,函数必须被定义或者被声明。函数的参数都需要
经过检验。因此一个函数的参数列表为空()的函数声明并不表示一个函数的声明是不确定的,
相反,它表示这个函数不接收任何参数。同时,函数的参数必须完全匹配,不会进行任何自动
转化。因此选择哪一个重载函数是非常明确的。 
和 C++一样,使用 return 退出一个函数,一个不是返回空类型(novoid)的函数必须返回一个
值。实际返回值的类型必须和声明中的返回值类型完全匹配 
函数不能递归的调用,无论是直接的还是间接的。 
3.6.2 调用协定
GLSL 使用传值(call by value)和传递返回值(call by return)的调用协定,call by value
和 C 类似,被指定为 input 的参数的值被拷贝一份后,将拷贝传递给函数,而不是传的引用。
因为没有指针,所以不需要担心参数和其他变量指向同一段内存(aliase)。Call by return 部
分表示一个被修饰为 output 的参数将作为返回值返回给函数的调用者,在函数返回时候,被调
用函数把数据回写到这个变量,并返回给调用者。 
为了指明一个参数何时进行拷贝,需要使用关键字 in , out 或者 inout 来修饰。对只需要传
递给函数,而不需要返回的,使用 in。in 也是默认的修饰符,如果没有指定,默认为 in。如
果一个参数不是传递给函数,而是从函数里得到返回值的话,使用 out。既要传递给函数,也
要从函数得到返回值的话,使用 inout。 
in 传递给函数,但是不作为返回值,但是在函数里依然可写。 
out 只作为返回值,可读,但是在函数入口出读出的数据值无定义。 
inout 传入和返回。 
Const 限定符也可以在函数的参数上使用,这里,并不是以为这个变量在编译期是个常数,它
表示由 const 修饰的变量在这个函数里不允许被改变。注意到一个普通的,没有限定符的,仅
仅用做输入的变量是可写的,在函数返回的时候不需要传递回数据给调用者。因此,在这里,
const in 和 in(或者没有限定符的变量)是有区别的,当然 out 和 inout 不能声明成 const。 
例子:: 
void ComputeCoord(in vec3 normal,  // 'normal'被传递给函数,可读,可写,但是不能
                                   // 给调用者返回数据。 
                  vec3 tangent,    // 等效于有 in 修饰符 
                  inout vec3 coord)// 传入和传出 
Or, vec3 ComputeCoord(const vec3 normal,// normal 不可写 
                  vec3 tangent, 
                  in vec3 coord)    //函数有返回值 
以下的写法是不允许的。: 
void ComputeCoord(const out vec3 normal, //非法; normal 不可写 
                  const inout vec3 tang, //非法; tang 不可写 
                  in out vec3 coord)     //非法; 应该使用 inout 
函数可以返回一个值或者不返回任何东西,如果一个函数不返回任何数据,则必须声明为 void
类型。如果函数有返回值,返回值可以是出数组以外的任何类型。结构也可以作为返回值,结
构里可以包含数组。 
3.6.3 内建函数
有大量的内建的函数可以使用,他们都在第 5 章里有详细介绍。 
这些函数也是可以被重载,提供自定义的实现。重载一个函数的时候,只需要在调用时候的定
义域里提供函数的原型或者实现,编译器和连接器会寻找自定义的函数版本来解决这个调用。
举例来说,一个内建的函数声明如下: 
float sin(float x); 
如果 Shader 想试验一下精度或者性能,或者在特定的领域特例化一个 sin 函数,我们可以使
用这样的方式重载: 
float sin(float x) 

    return <.. some function of x..> 

void main() 

    // 调用自定义的 sin,而不是内建的 sin 
    float s = sin(x); 

这和标准语言的连接技术类似,使用一个函数库的时候,我们首先有一个局部定义域,这个定
义域里的函数将先于库里的函数被使用。如果一个函数在不同的 shader 里定义,那么应该保证
有一个原形在使用这个函数前被声明,否则将使用内建的函数版本。 
3.7 操作符
表 3.1 包含了 GLSL 里可用的操作符,以操作符号的优先级排列,优先级和相互关系和 C 语言一
样。 表 3.1.  操作符,以操作符的优先级排序
操作符 描述
[ ] 索引 
.  成员选取和 swizzle 
++ -- 后增/减 
++ -- 前增/减 
- ! 取负和取反 
* / 乘除 
+ - 加减 
< > <= >= 比较操作符 
== != 等于判断 
&& 逻辑与 
^^ 逻辑异或 
|| 逻辑或 
?: 选择 
= += -= *= /= 赋值 
,  ,操作符(Sequence) 
3.7.1 索引
向量,矩阵和数组可以用下标操作符来索引,所有的索引都是从 0 开始的。第一个元素在索引
0 的位置。索引一个数组的方法和 C 语言一样。 
对向量进行索引将返回一个标量形式的分量。这允许给每个分量一个数字形式的名字:0,1,…,
同时也允许用名字索引的形式来访问它的分量。例如: 
vec4 v = vec4(1.0, 2.0, 3.0, 4.0); 
float f = v[2];  // f takes the value 3.0 
这里, v[2] 的值为浮点形式的标量 3.0, v[2]被赋值给 f。 
矩阵的索引结果是一个列向量。例如: 
mat4 m = mat4(3.0);  // 把对角线位置初始化为 3.0 
vec4 v; v = m[1];   // v 的值现在是(0.0, 3.0, 0.0, 0.0)  
这里 m 的第二列,m[1]被当作一个向量被赋值给 v。 
如果用一个小于 0,或者大于对象大小的下标去索引数组、向量或者矩阵,这样的行为是未定
义的。 
3.7.2 Swizzling 
标准的结构成员选择操作符(.)也可以用来 SWZIZZLE 一个向量的分量。在选择的
时候,只要在 Swzille 操作符(.)后按不同的顺序列出向量的分量,分量可以以重新
排列后的顺序被选择。例如: 
vec4 v4; 
v4.rgba;  // 一个 vec4 变量,和 v4 这样的用法相同 
v4.rgb;   // is a vec3, 
v4.b;     //一个浮点, 
v4.xy;    // 一个 vec2, 
v4.xgba;  // 非法,名字不是来自于一个相同的名字集合。 
分量的名字顺序可以重新排列,甚至可以重复: 
vec4 pos = vec4(1.0, 2.0, 3.0, 4.0); 
vec4 swiz = pos.wzyx; // swiz = (4.0, 3.0, 2.0, 1.0) 
vec4 dup = pos.xxyy;  // dup = (1.0, 1.0, 2.0, 2.0) 
最多有四个分量的名字可以以 swizzle 的方式列出。这种方法将产生出一种不存在的类型。
Swizzling 的规则在作为左值(用来写)和右值(用来读)的时候,是有一些细微的区别的。
作为右值的时候,可以任意组合。作为左值的时候,分量名字不能重复出现。例如: 
vec4 pos = vec4(1.0, 2.0, 3.0, 4.0); 
pos.xw = vec2(5.0, 6.0); // pos = (5.0, 2.0, 3.0, 6.0) 
pos.wx = vec2(7.0, 8.0); // pos = (8.0, 2.0, 3.0, 7.0) 
pos.xx = vec2(3.0, 4.0); //非法,x 出现两次。 
作为右值,这样的语法可以用在任何一个可以产生向量类型的表达式上。比如,从
一个纹理查找结果中产生一个有 2 个分量的向量: 
vec2 v = texture1D(sampler, coord).xy; 
内建函数 textureID 返回一个 vec4. 
3.7.3 逐分量操作
有点例外的地方是。当一个对一个运算符号被施加在一个向量上时候,等效于这个操作符施加
在向量的每个分量上。例如: vec3 v, u; 
float f; 
v = u + f; 
等效于 
v.x = u.x + f; 
v.y = u.y + f; 
v.z = u.z + f; 
和 
vec3 v, u, w; 
w = v + u; 
等效于 
w.x = v.x + u.x; 
w.y = v.y + u.y; 
w.z = v.z + u.z; 
如果一个二元运算符被施加在一个向量和一个标量上,那么这个标量将和向量的每个分量进行
运算。如果两个向量参加运算。它们的大小(维数)必须相同。 
一个例外的情况是当一个向量和一个矩阵相乘,或者两个矩阵相乘,他们遵照标准的线性代数
的规则,而不是逐分量的运算。 
递增和递减运算符(++和—),以及取反运算符的作用和规则和 C 语言里的一样。如果被施加到
一个向量和矩阵上的时候,将会进行逐分量的递增和递减操作。这几个运算符只能对整数或者
浮点类型上。 
加(+)、减(-)、乘(*)、除(/)运算符的作用和C语言里一样。都进行逐分量的运算。除了前面提
到的线性代数里的乘法一样。 
vec4 v, u; 
mat4 m; 
v * u;  // 逐分量相乘 
v * m;  // 线性代数里的行向量乘矩阵。 
m * v;  //线性代数里的矩阵乘列向量。 
m * m;  //线性代数里的矩阵相乘 
其他的运算符都是逐分量的。 
逻辑非(!),逻辑与(&&),逻辑或(||)以及逻辑异或运算符,只能对 boolean 型标量进行操作。
它们的运算结果也是个 boolean 型的变量。它们不能对向量进行操作。没有一个内建的函数,
可以用来计算 boolean 型向量的逻辑运算。 比较运算符(<,>,<=,和>=)只能对浮点和整数类型进行操作,运算结果是一个 boolean 型。有内
建的函数,比如 lessThanEqual,可以用来逐分量的比较两个向量,其结果是一个 boolean 向
量。 
相等运算符(==,!=)可以对除了数组以外的任何类型进行操作,它对每个分量或者结构体的每
个成员进行测试,测试结果是个 boolean 型,用来指示两个操作数是不是相等。如果两个操作
数相等,首先,它们的类型必须相等。它们的每个分量或者成员必须相等。如果要得到一个逐
分量比较的结果,就得使用内建的函数 equal 和 notEqual(==和!=返回的是标量,不是一个向
量)。 
等于(==)、不等于(!=) 、比较运算符(<, >, <=, and >=)、逻辑运算符(!)都产生一个boolean
型的标量。因为流程控制语句需要一boolean型的标量。如果一个内建的函数equal等产生了一
个boolean型的向量,我们可以通过内建的函数any和all来把这个向量转化成一个标量。例如:
要求一个向量中是不是有分量小于另一个向量的对应分量: 
vec4 u, v; 
... 
if (any(lessThan(u, v))) 
    ... 
赋值运算符(=)需要左边和右边的类型严格的匹配,除了数组以外的任何类型,都可
以被赋值。其它的赋值运算符(+=, -=, *=, and /=)和C语言类似,但是展开后语义上
必须合法。 
a *= b     a = a * b 
这里,a*b 在语义上必须是合法的,a*b 结果的类型必须和 a 的类型相同。其他的赋值也
类似 
三重选择运算符(?:)是一个三目运算符(exp1 ? exp2 : exp3)。这个运算符先计算出第
一个表达式,它必须是个boolean型的标量。如果结果为真,则选择计算第二个表达式,并将结
果作为选择符的运算结果。如果为假,则选择计算第三个表达式,第二和三两个表达式里只有
一个会被计算。第二和三两个表达式的类型必须一样,它们可以是除了数组以外的任何类型。
运算符的返回值类型就是第二和三两个表达式的类型。 
序列运算符(逗号,)返回以逗号隔开的、右边优先的表达式列表的值和类型,所有的表
达式从左到右的顺序计算,返回值为最后一个表达式的值。 
3.8 Preprocessor 预处理器
预处理器和C语言类似,支持如下的预处理指令如下: 
#define 
#undef 
#if #ifdef 
#ifndef 
#else 
#elif 
#endif
as well as the defined operator are exactly as in standard C. This includes macros with 
arguments and macro expansion. Built-in macros are 
__LINE__
__FILE__
__VERSION__
__LINE__ substitutes a decimal integer constant that is one more than the number of 
preceding new-lines in the current source string. 
__LINE__用来代替一个表示当前行号的整型常数, 
__FILE__用来代替一个十进制整数,表示当前处理的是哪个源代码字符串号。 
__VERSION__用来代替一个十进制整数,表示当前的OpenGL Shading Language的版本号,本书
里的OpenGL Shading Language版本号是 100. 
同时还支持以下指令: 
#error message 
#line 
#pragma
#error 输出信息到shader的information log里。如果一个语义错误产生的时候。编译器然后
将收到这个信息,  
#pragma 是和GLSL的实现相关的,如果一个GLSL实现不支持这个#pragma参数,将简单的忽略掉。
但是下列pragmas是可以移植的。 
使用优化pragma指令。 
#pragma optimize(on) 
#pragma optimize(off) 
打开和关闭优化开关,可以帮助开发和调试shader。只能在函数外部指定,默认情况下,
所有的优化开关都是关闭的。
调试pragma 
#pragma debug(on) 
#pragma debug(off) 打开#pragma debug 开关,可以向 shader 写入 debug 信息,这些信息可以被除错器使用,
#pragma debug 只能在函数以外使用,默认的情况下,该值为 off。 
#line 在宏展定义的时候,必须是以下两中形式中的一种。 
#line line 
#line line source-string-number 
这里的 line 和 source-string-number 都是 constant integer 的表达式,当处理到这个指
令 的 时 候 ( 包 括 下 一 个 新 行 ) , GLSL 的 实 现 将 认 为 当 前 处 理 的 是 源 代 码 字 串 号为
source-string-number,行号为 line。以后的源代码字串号将一直为 source-string-number,
直到下一个#line 指令指定新的行号和新的源代码字串号。 
3.9  预处理器表达式
预处理器表达式可以有表 3.2 里的运算符: 
表 3.2.  预处理器表达式
运算符 描述
+ - ~ ! defined 一元运算符 
* / % 乘除,求摸 
+ - 加和减 
<< >> 移位 
< > <= >= 比较 
== != 相等测试 
& ^ | 位操作 
&& || 逻辑运算 
优先级和行为,和标准 C 语言的预处理器一样 
对预处理器表达式有一点必须记住,它们是在运行编译器(GLSL 的编译器)的处理器上运行的,
而不是运行 shader 的图形处理器。运行编译器的处理器支持的数据精度都有效。and hence will 
likely be different from the precision available when executing expressions in the core 
language. 在语言的核心,字符串(string)类型是不支持的,#,##运算符不支持,sizeof 预处理器指令也
不支持。 
3.10 Error Handling 错误处理
编译器可以接收一些不正常的程序,因为我们不可能让编译器检测到所有的错误。例如,非常
精确的检测一个变量在使用前是不是被初始化是不现实的。类似病态的程序在不同的平台上执
行起来可能不同。所以,OpenGL Shading Language 的规范只保证正确的程序的可移植性。 
我们鼓励 GLSL 的编译器能检测病态的程序,并输出诊断信息,但是这样的要求不是必须的。编
译器要求能够处理词法,语法,语义上的错误,并返回错误信息。Shader 一旦产生这些错误,
就不能往下编译了。用来获得编译器诊断信息的 OpenGL 函数在第 7.5 节讨论。 
3.11 Summary 总结
OpenGL Shading Language 是一种针对 OpenGL 环境设计的高级过程语言。这种语言允许程序使
用可编程的、并行的图形硬件。它让一个熟悉 C/C++的程序员能简洁方便的描述图形着色算法。
OpenGL Shading Language 支持标量(scalar),向量(vector),矩阵,结构体,和数组。取样
器类型被用来访问纹理。数据类型限定符用来定义 shader 的输入和输出,用来初始化的构造器,
类型转化以及类似 C/C++的流程控制。 
3.12 更进一步的资料
OpenGL Shading Language在文档《The OpenGL Shading Language, Version 1.051》Kessenich, 
Baldwin, and Rost (2003).里定义。(注:可以从www.OpenGL.org下载) 
OpenGL Shading Language 的语法全部包括在附录 A 里。这两个文档可以用来当作语言本身的
参考。其他的教程,幻灯片,以及白皮书可以到 3DLabs 的网站去下载 
OpenGL Shading Language 的功能是用扩展的形式提供和支持的。阅读这些扩展的规范和 OpenGL
本身的规范有助于更深刻的了解支持 OpenGL Shading Language 的系统。OpenGL 参考的最后一
章对了解 OpenGL 也很有帮助。 
C语言的标准参考手册是《The C Programming Language》Brian Kernighan and Dennis Ritchie 
(1988),由C语言的设计者编写的。同样的,C++的参考手册是C++之父Bjarne Stroustrup 所著
的《The C++ Programming Language》 Bjarne Stroustrup (2000)。还有其他许多著名的书,
可以当作C/C++的参考。 
[1] 3Dlabs developer Web site. http://www.3dlabs.com/support/developer[2] Kernighan, Brian, and Dennis Ritchie, The C Programming Language, Second Edition, 
Prentice Hall, Englewood Cliffs, New Jersey, 1988. 
[3] Kessenich, John, Dave Baldwin, and Randi Rost, The OpenGL Shading Language, Version 
1.051, 3Dlabs, February 2003. http://www.3dlabs.com/support/developer/ogl2
[4] OpenGL Architecture Review Board, ARB_vertex_shader Extension Specification, 
OpenGL Extension Registry. http://oss.sgi.com/projects/ogl-sample/registry
[5] OpenGL Architecture Review Board, ARB_fragment_shader Extension Specification, 
OpenGL Extension Registry. http://oss.sgi.com/projects/ogl-sample/registry
[6] OpenGL Architecture Review Board, ARB_shader_objects Extension Specification, 
OpenGL Extension Registry. http://oss.sgi.com/projects/ogl-sample/registry
[7] Segal, Mark, and Kurt Akeley, The OpenGL Graphics System: A Specification (Version 
1.5), Editor (v1.1): Chris Frazier, Editor (v1.2–1.5): Jon Leech, July 2003. 
http://opengl.org
[8] Stroustrup, Bjarne, The C++ Programming Language (Special 3rd Edition), 
Addison-Wesley, Reading, Massachusetts, 2000. 
0 0
原创粉丝点击