GLSL是OpenGL Shading Language的缩写,意为OpenGL着色器语言。在学习着色器语言之前,首先要了解OpenGL的渲染管线(网上很多资料),这里不作赘述,本文将会详细介绍glsl中的各种变量的类型以及变量基本操作。
GLSL具备了C++和Java的很多语言特性,这些特性后面会提到。创建一个着色器程序很简单,如下所示:
#version 330 core
void main(){
//在此编写着色器代码
}
这就是一个着色器的基础结构,但是它是一个空的,所以接下来就要开始介绍变量类型。
1.标量类型(scalars):最基础的数据类型,主要有五种
float : 32为浮点数,浮点数字面量必须包含一个小数点,最好添加一个“f”后缀;
double:64位浮点数,必须在后面添加“lF”或“LF”后缀;
int:有符号32位整数;
uint:无符号32为整数,要在后面添加“u”后缀;
bool:布尔值。
2.向量和矩阵(vectors and matrices): 向量和矩阵由标量类型聚合而成
vecn: 由单精度浮点类型组成的向量
dvecn: 由双精度浮点类型组成的向量
ivecn: 由有符号整型组成的向量
uvecn: 由无符号整型组成的向量
bvecn: 由布尔类型组成的向量
其中n为2,3,4。
向量的访问有两种方式:使用分量访问符或者数组访问方式,这里主要介绍一下分量访问符的使用。
分量访问符一共有三种类型,在一个变量中,一次只能使用一种类型的访问符,也就是不能“串着用”。
分量访问符 | 符号描述 |
(x,y,z,w) | 与位置相关的分量 |
(r,g,b,a) | 与颜色相关的分量 |
(s,t,p,q) | 与纹理坐标相关的分量 |
swizzling: 分量访问符的一种主要应用,可以使用分量访问符去访问向量各个位置的数据,如下:
vec2 p;
vec4 pos=p.xyxy;
它可以使用得到的分量进行“任意搭配”,这也是它被称作swizzle的原因。
矩阵:所有的矩阵类型都是浮点类型,要么是单精度,要么是双精度,(在OpenGL4.0以后,双精度的矩阵使用dmat来声明)。
matnxm:代表一个n列m行的矩阵,n和m都为2,3,4
matn:代表一个n行n列的矩阵,n为2,3,4
注意:OpenGL uses column-major matrices,OpenGL使用的是列主序(DX使用的是行主序),如果对列主序和行主序不清楚最好找点资料看一下,其主要影响的是数据在内存中的排列方式。
Opaque types:不透明类型,Opaque types represent some external object which the shader references in some fashion.
不透明类型代表着色器以某种方式引用的一些外部对象,主要包括采样器、图像以及原子计数器,这些内容之后会详细介绍。
之前的标量类型以及向量矩阵都是透明的,它们的内部形式都是暴露出来的。
隐式类型转换(implicit conversion)
glsl比C++更加注重类型安全,因此它支持更少的隐式类型转换,如下所示。
所需类型 | 可以从这些类型隐式转换 |
uint | int |
float | int ,uint |
double | int,uint,float |
除此之外,其它所有的数值转换都需要提供显式的转换构造函数(explicit conversion construtor),每种构造函数可以传入多重其它类型的值进行显式转换,这也体现了glsl的另一特性:函数重载。
数组:和C/C++中的使用是类似的,它支持任意类型的数组,包括结构体数组,数组数组(即二维数组)以及不透明类型的数组。
同时,glsl与java类似,它有一个隐式的方法可以返回元素的个数:即取长度的方法length(),使用length可以获得一个编译时常量,前提是该数组在length方法前就已经确定。
结构体:结构体的定义是不支持匿名的,所以必须要指定名称,而且不可以在一个结构体的内部定义一个新结构体,但是可以使用结构体类型变量作为成员,它还可以包含不透明类型的变量,注意:结构体不能用于输入和输出变量。
如果需要引用结构体中的某个元素,可以直接使用“.”符号。
绝大多数的变量在声明的同时必须进行初始化,但是有一下三种情况时例外的:
第一种:被in和out限定符修饰的变量(关于OpenGL中的限定符之后会专门讲述);
第二种:所有不透明类型的变量
第三种:在接口块中声明的变量
1.标量的初始化是使用一个字面量,并且要根据不同的类型有选择的加上不同的后缀名。
2.构造函数:比较复杂的一些基本类型,类似于向量和矩阵,需要使用构造函数,所有构造函数的基本形式都源于
typename(value1,value2,...)
typename就是要构造的变量的名称,而value则可以为一个字面量,一个预定义变量或者一个构造函数。
3.向量构造函数
//vec3所有的三个值都从1.0构造而来
vec3(1.0)=vec3(1.0,1.0,1.0);
//vec4的构造函数值可以是字面量或其它构造函数
vec4(1.0,2.0,3.0,2.0)=vec3(1.0,vec2(2.0,3.0),2.0);
4.矩阵构造函数
如果一个对角矩阵使用一个单个的字面量的值来构造的话,这个值将用来初始化对角线的值,剩余的值都为0,因此mat4(1.0)就是一个4x4的单位矩阵。如果使用多个值来构造,它将会按照列主序进行初始化。
如果构造函数使用的是另一个矩阵A,那么就会将A的值拷贝到矩阵的对应位置上,剩余的则使用单位矩阵来进行填充。
mat3 matrixA=mat3(3.0);
//matrixB的对角线的前三个值为3.0,最后一个值为1.0
mat4 matrixB=mat4(matrixA);
5.数组构造函数
float array[3]=float[3](1.2,2.4,3.5);
float array[]=float[](1.2,2.4,3.5);
数组的大小不是必须指定的,它可以根据构造函数中的参数来确定。
6.结构体构造函数
struct Data{
float first
vec2 sec;
};
Data data1=Data(1.0,vec2(2.0,2.2));
Data dataArrays[3]=Data[3](Data(1.0,vec2(2.0,2.1)),
Data(2.0,vec2(3.0,2.3)),
Data(3.0,vec2(4.0,2.5)));
7.初始化列表(initializer lists)
考虑上面dataArrays的初始化操作,显得有些冗余,因为我们要进行三次构造函数的重复工作,如果使用初始化列表,
Data dataArrays[3]={{1.0,{2.0,2.1}},
{2.0,{3.0,2.3}},
{3.0,{4.0,2.5}}};
这样就不会重复使用Data的构造函数,而编译器则会依据Data的结构体类型来正确推断数据的类型,以此进行初始化工作。
初始化列表使用“{}”作为分界符。
操作符所对应的类型必须是相同的,并且对于向量和矩阵而言,操作符的操作对象的维度也必须相同。在glsl中,大部分操作符都是经过重载的,也就是它们可以用来处理多种类型的数据。
需要注意的是,如果要进行向量和矩阵之间的乘法或者矩阵与矩阵之间的乘法,要注意矩阵和向量的维度必须是匹配的,而且要注意操作数顺序,因为矩阵乘法不满足交换律。
注意:在glsl中,使用操作符进行两个向量的相乘,得到的结果是一个逐分量相乘的向量,如果想要进行点乘和叉乘,则需要使用相应的函数。