GLSL初识

2,584 阅读11分钟

什么是GLSL?

        GLSL 全称为 OpenGL Shading Language ,是用来在OpenGL中编写着色器程序的语言。用GLSL编写的着色器程序是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。比如:视图转换、投影转换、颜色混合等等。GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment(片元着色器),有时还会有Geometry Shader(几何着色器)。

GLSL语法初探

标识符

      标识符其实就是程序员自定义的变量名、函数名等等。变量名可以由下划线、数组、字母组成,我们可以随便用下划线、数组、字母定义变量名但是需要注意一下几点:

  1. 不能以数字开头
  2. 不能以gl_开头,gl_是GLSL保留的前缀,用于GLSL的内部变量,比如gl_Position等等;
  3. 另外关键字和一些GLSL内部保留的名称也是不能够作为变量名的

数据类型

     在GLSL中仅支持下列基础数据类型,需要注意的是与C语言相比GLSL中并没有字符、字符串、指针这些类型。

Type

Meaning

void

for functions that do not return a value

bool

a conditional type, taking on values of true or false

int

a signed integer

float

a single floating-point scalar

vec2

a two component floating-point vector

vec3

a three component floating-point vector

vec4

a four component floating-point vector

bvec2

a two component Boolean vector

bvec3

a three component Boolean vector

bvec4

a four component Boolean vector

ivec2

a two component integer vector

ivec3

a three component integer vector

ivec4

a four component integer vector

mat2

a 2×2 floating-point matrix

mat3

a 3×3 floating-point matrix

mat4

a 4×4 floating-point matrix

mat2x2

same as a mat2

mat2x3

a floating-point matrix with 2 columns and 3 rows

mat2x4

a floating-point matrix with 2 columns and 4 rows

mat3x2

a floating-point matrix with 3 columns and 2 rows

mat3x3

same as a mat3

mat3x4

a floating-point matrix with 3 columns and 4 rows

mat4x2

a floating-point matrix with 4 columns and 2 rows

mat4x3

a floating-point matrix with 4 columns and 3 rows

mat4x4

same as a mat4

sampler1D

a handle for accessing a 1D texture

sampler2D

a handle for accessing a 2D texture

sampler3D

a handle for accessing a 3D texture

samplerCube

a handle for accessing a cube mapped texture

sampler1DShadow

a handle for accessing a 1D depth texture with comparison

sampler2DShadow

a handle for accessing a 2D depth texture with comparison

      除了以上这些基础数据类型,GLSL中还支持由以上这些类型组合而成的结构体、数组等复合数据类型

     可以用struct 关键字将已定义的数据类型聚合到一起形成结构体,比如:

struct light {
 float intensity;
 vec3 position;
} lightVar;

     结构体具体用法和C语言差不多这里就不多赘叙了;数组一样具体用法和C语言差不多,只不过在GLSL中不支持多维数组,也就是说数组的元素不能是数组。

存储限定符

GLSL中变量的定义可以在类型前面指定存储限定符,例如:

attribute vec4 position
uniform vec3 color = vec3(0.7, 0.7, 0.2)

GLSL中常见的存储限定符如下表:

限定符

意思

< none: default >

默认的,可省略,这种情况下仅仅提供一个对相关处理器上内存的读写权限

const

编译时常量,其值必须在声明时初始化,或只读的函数参数。

attribute

为每个顶点数据连接顶点着色器和OpenGL。所以attribute表示只读的顶点数据,仅用在顶点着色器中。其数据来自当前的顶点状态或者顶点数组。它必须是全局范围声明的,不能再函数内部。attribute修饰的变量可以是浮点数类型的标量,向量,或者矩阵。不可以是数组或则结构体

uniform

统一变量。在着色器执行期间统一变量的值是不变的,与const常量不同的是,这个值在编译时期是未知的,是由着色器外部初始化的。统一变量在顶点着色器和片段着色器之间是共享的。它也只能在全局范围进行声明。

varying

顶点着色器的输出。例如颜色或者纹理坐标,(插值后的数据)作为片段着色器的只读输入数据。必须是全局范围声明的全局变量。可以是浮点数类型的标量,向量,矩阵。不能是数组或者结构体。

centroid varying

在没有多重采样的情况下,与varying是一样的意思。在多重采样时,centorid varying在光栅化的图形内部进行求值而不是在片段中心的固定位置求值。

  函数参数限定符   

限定符

说明

< none: default >

默认使用 in 限定符

in

in函数参数的默认限定符,传入形参的其实是实参的一份拷贝.在函数中,修改in修饰的形参不会影响到实参变量本身

out

out作用是向函数外部传递新值,out传递进来的参数是write-only的,就像是一个"坑位",坑位中的值需要函数给他赋予. 在函数中,修改out修饰的形参会影响到实参本身.

inout

inout 可以被理解为是一个带值的"坑位",及可读也可写,在函数中,修改inout修饰的形参会影响到实参本身.

内置变量

       GLSL内部为我们预先定义了一批具有特殊含义的变量,方便开发者使用,这些变量就是内置变量,在顶点着色器中有着如下的内置变量:

名称

类型

描述

gl_Color

vec4

输入属性-表示顶点的主颜色

gl_SecondaryColor

vec4

输入属性-表示顶点的辅助颜色

gl_Normal

vec3

输入属性-表示顶点的法线值

gl_Vertex

vec4

输入属性-表示物体空间的顶点位置

gl_MultiTexCoordn

vec4

输入属性-表示顶点的第n个纹理的坐标

gl_FogCoord

float

输入属性-表示顶点的雾坐标

gl_Position

vec4

输出属性-变换后的顶点的位置,用于后面的固定的裁剪等操作。所有的顶点着色器都必须写这个值。

gl_ClipVertex

vec4

输出坐标,用于用户裁剪平面的裁剪

gl_PointSize

float

点的大小

gl_FrontColor

vec4

正面的主颜色的varying输出

gl_BackColor

vec4

背面主颜色的varying输出

gl_FrontSecondaryColor

vec4

正面的辅助颜色的varying输出

gl_BackSecondaryColor

vec4

背面的辅助颜色的varying输出

gl_TexCoord[]

vec4

纹理坐标的数组varying输出

gl_FogFragCoord

float

雾坐标的varying输出

片元着色器中内置变量:

名称

类型

描述

gl_Color

vec4

包含主颜色的插值,只读输入

gl_SecondaryColor

vec4

包含辅助颜色的插值,只读输入

gl_TexCoord[]

vec4

包含纹理坐标数组的插值,只读输入

gl_FogFragCoord

float

包含雾坐标的插值,只读输入

gl_FragCoord

vec4

窗口的x,y,z和1/w,只读输入

gl_FrontFacing

bool

只读输入,如果是窗口正面图元的一部分,则这个值为true

gl_PointCoord

vec2

点精灵的二维空间坐标范围在(0.0, 0.0)到(1.0, 1.0)之间,仅用于点图元和点精灵开启的情况下。

gl_FragData[]

vec4

使用glDrawBuffers输出的数据数组。不能与gl_FragColor结合使用。

gl_FragColor

vec4

输出的颜色用于随后的像素操作

gl_FragDepth

float

输出的深度用于随后的像素操作,如果这个值没有被写,则使用固定功能管线的深度值代替

操作符

操作符

描述

()

用于表达式组合,函数调用,构造

[]

数组下标,向量或矩阵的选择器

.

结构体和向量的成员选择

++ –

前缀或后缀的自增自减操作符

+ – !

一元操作符,表示正 负 逻辑非

* /

乘 除操作符

+ -

二元操作符 表示加 减操作

<> <= >= == !=

小于,大于,小于等于, 大于等于,等于,不等于 判断符

&& || ^^

逻辑与 ,或, 异或

?:

条件判断符

= += –= *= /=

赋值操作符

,

表示序列

        需要注意的是取地址符&和解引用的 * 操作在GLSL中是不存在的,因为GLSL不能直接操作地址。类型转换操作也是不允许的。 位操作符(&,|,^,~, <<, >> ,&=, |=, ^=, <<=, >>=)是GLSL保留的操作符,将来可能会被使用。还有求模操作(%,%=)也是保留的。

结构体和数组的操作

      结构体的字段以及数组的长度都是通过点(.)语法来访问的,与c语言类似,对应结构体和数组仅一下几种操作是合法的:

field or method selector

相等判断

== !=

赋值

=

索引访问[仅数组]

[]

需要注意的是相等判断和赋值运算都必须是两个类型一样,size相同的才能进行。

向量与矩阵操作

      除了极少数例外,通常对于向量与矩阵的操作都等价于分别对其相应分量做出同样的操作。比如:

vec3 u,v;
float f;
v = u + f;

上面这段代码就等价于:

vec3 u,v;
float 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;

      其他的操作这里就不做多的叙述,大家可以参考线性代数或者3D数学。这里简单说一下向量分量的访问:

      向量中单独的分量可以通过{x,y,z,w},{r,g,b,a}或者{s,t,p,q}的记法来表示。这些不同的记法用于顶点坐标,颜色成分,纹理坐标。在分量访问中,不可以混合使用这些记法。其中{s,t,p,q}中的p替换了纹理的r坐标,因为与颜色r重复了。比如:

vec4 v4;
v4.rgba; // is a vec4 and the same as just using v4,
v4.rgb; // is a vec3,
v4.b; // is a float,
v4.xy; // is a vec2,
v4.xgba; // is illegal 

分量的顺序可以交换也可以重复:比如:

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)

这种表达方式同样可以用到表达式的左边,只是不允许重复:

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); // illegal - 'x' used twice
pos.xy = vec3(1.0, 2.0, 3.0); // illegal - mismatch between vec2 and vec3

       对于矩阵可以看成是向量的数组,可以使用数组下标语法访问矩阵的分量。 应用单个下标 到矩阵将矩阵视为列向量的数组,并选择一个单列,其类型为与矩阵大小相同的向量。 最左边的列是列0。然后第二个下标如先前为向量所定义,对列向量进行运算。 因此,两个下标选择了一列, 然后又选择了该列的一行。比如:

mat4 m;
m[1] = vec4(2.0); // sets the second column to all 2.0
m[0][0] = 1.0; // sets the upper left element to 1.0
m[2][3] = 2.0; // sets the 4th element of the third column to 2.0

流程控制

条件控制:也就是常说的if else 语句,需要注意的是,if(条件)的条件只能是bool类型的,并且不能在if else中声明变量,变量必须声明在外面,比如:

vec4 color = unlitColor;
if (numLights > 0)
{
    color = litColor;
}else{
    color = unlitColor;
}

循环控制:与C语言类似支持for,while,do-while 三种写法,可以用contine跳出本次循环,用break跳出所有循环。

discard控制:片元着色器中有一种特殊的控制流成为discard。使用discard会退出片元着色器,不执行后面的片元着色操作,也不会写入帧缓冲区。

函数

      在每个shader中必须有且只能有一个main函数。GLSL中的函数,必须是在全局范围定义和声明。不能在函数定义中声明或定义函数。函数必须有返回类型,参数是可选的。参数的修饰符(in, out, inout, const等)是可选的。

       结构体和数组也可以作为函数的参数。如果是数组作为函数的参数,则必须制定其大小。在调用传参时,只传数组名就可以了。

另外需要注意一下几点:

  • 函数名可以通过参数类型重载,但是和返回类型值无关;
  • 所有参数必须完全匹配,参数不会自动;
  • 函数不能被递归调用;
  • 函数返回值不能是数组

      最后这里我们只是说了一下GLSL的一些基本语法,不过对于iOS开发者来说其实已经基本足够了,如果想要深入了解GLSL可以参考官方文档