这篇文章总结了一些GLSL
语言的基础知识
GLSL
总体上的写法和C语言相似
变量
声明变量的格式是这样的:
格式是变量类型 变量名=值;
比如我声明一个foo
:
float foo=1.;
这样我就给float
浮点类型的数字变量foo
赋值了一个1.0
在写的时候是可以忽视0的,1.0可以写成1.,0.5可以写成.5
浮点型变量必须要有小数点,不能省略!
在写GLSL
的时候一定要记得:
语句结尾一定要加一个分号;。这是新手最容易会犯的错误。
浮点型float
可以说是GLSL
里最常用的类型。
除此之外,还有比较常用的是int
整型,代表了整数,它无需加小数点
int foo=1;
还有一种bool布尔型,代表逻辑值,即真(true)或假(false),通常用于条件判断和控制流程。
bool foolBool = true;
bool bar = fragCoord.x < iResolution.x * .25;
if (bar) {
fragColor = vec4(1., 0., 0., 1.);
} else {
fragColor = vec4(0., 1., 0., 1.);
}
上面提到的变量都是一维变量(标量),如果想要声明更高维度的变量的话,就要用到vec
类型(也就是向量类型)。
vec2 a=vec2(1.,0.);
vec3 b=vec3(1.,.5,0.);
vec4 c=vec4(1.,.5,0.,1.);
vec
类型支持 3 种维度:二维vec2
、三维vec3
和四维vec4
,四个维度分别为x
、y
、z
、w
(也可以写为r、g、b、a)。如果想对多维度变量进行取值或赋值操作,就要用到.符号,并且四个维度可以任意组合(称为Swizzling
)。
vec2 d=b.xy;// d的值为vec2(1.,.5)
d.y=2.;// d的值为vec2(1.,2.)
vec3 e=c.yxy;// e的值为vec3(.5,1.,.5),c.yxy可以理解为vec3(c.y,c.x,c.y)
e.zx=vec2(1.);// e的值为vec3(1.,1.,1.)
除了vec
类型有更高的维度支持外,mat
类型(也就是矩阵类型)亦是如此。mat2
类型代表了一个大小是2x2
的矩阵,mat3
类型则代表了一个3x3
的矩阵、mat4
类型是4x4
的矩阵。
- 矩阵是一个二维数组,它的每一行代表一个向量。
- 矩阵的每一列代表一个向量的分量。
- 矩阵的行数和列数决定了矩阵的维度。
- 矩阵的维度是一个二维向量,它的x分量代表行数,y分量代表列数。
mat2 f = mat2(1., 0., 0., 1.);
mat3 g = mat3(1., 0., 0., 0., 1., 0., 0., 0., 1.);
mat4 h = mat4(1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.);
而我们要调用它也是很简单的:
// 渲染矩阵
fragColor = vec4(f[0].x, f[0].y, f[1].x, f[1].y);
- 矩阵如何调用的?
- 矩阵的调用方式是矩阵名行号。
- 行号和列号都是从0开始的。
- 矩阵的行号和列号都是整数,所以可以用整数变量来调用矩阵的元素。
- f[0]就是矩阵f的第一行,f[1]就是矩阵f的第二行。
- f[0].x就是矩阵f的第一行的第一列,f[0].y就是矩阵f的第一行的第二列。
如果想把多个变量捆绑到一个变量上,可以使用结构体struct,它的使用方式如下所示
struct Foo {
vec2 a;
vec3 b;
vec4 c;
};
Foo i;
i.a = a;
i.b = b;
i.c = c;
fragColor = vec4(i.a, i.b.x, i.c.y);
顺便提一嘴,vec4的变量是可以合起来的,比如上面的代码示例,vec4四维变量可以用vec2(x,y)+ z + w来组合起来,很灵活
在创建变量时,我们也可以先声明变量,然后再对它进行赋值操作,并且赋值是从右到左进行的,因此,我们可以同时给多个变量赋予同一个值。
float one, two, three;
one = two = three = 1.;
运算符
算数运算符
执行基本的四则运算
float computA = 1.;
float computB = 2.;
float computC = computA + computB; // 3.
float computD = computA - computB; // -1.
float computE = computA * computB; // 2.
float computF = computA / computB; // 0.5
float g = mod(computA/computB) // 取余,值为1.
赋值运算符
用于将值赋给变量,赋值时也可同时进行算数运算。
(和一般编程语言差不多,比如a+=k这样的)
float varA, varB, varC, varD;
varA = varB = varC = varD = 3.;
float varE = 2.;
varA += varE; // varA = varA + varE; // 5.
varB -= varE; // varB = varB - varE; // 1.
varC *= varE; // varC = varC * varE; // 6.
varD /= varE; // varD = varD / varE; // 1.5
除此之外,还有比较运算符(>
,<
,==
等)、逻辑运算符(&&
,||
,!
等),这些运算符在一般的编程语言里也是有的,不多说了
唯一要注意的一点是:运算一定要保证维度的匹配,比如你不能将一个vec2
的变量与一个vec3
的变量相加,要加的话你得选取vec3
变量的其中 2 个维度,转成vec2
变量才能与另一个vec2
变量相加。
vec3 color = vec3(1., 0., 0.);
vec2 uv = fragCoord / iResolution.xy;
vec2 res = color.xy + uv;
当然,也有一种特殊的情况:当一个向量和一个标量进行运算时,GLSL
会将标量广播(broadcast)
到向量的每一个分量上。
当一个向量与一个标量进行数学运算时,这个标量会被自动扩展(或广播)到向量的每一个分量上。这意味着,标量的值将被应用于向量的每一个分量,执行相应的运算。
例如,考虑一个二维向量 vec2 和一个标量值。如果你进行减法运算,GLSL会将标量值分别减到向量的两个分量上:
vec2 a=vec2(1.);
a-=.5;// a的值为vec2(.5)
函数
当你想要复用一个代码片段时,只需要定义一个函数就行,比如说:
float add(float a,float b){
return a+b;
}
float c=add(1.,2.);// c的值为3.
函数要声明在main之外,并且从上到下调用,所以在main函数之前
一个函数包括返回类型(无返回类型为void
)、函数名、带类型的参数和返回值(可选)。调用函数时也要严格遵循定义好的类型。
当然,GLSL
本身也内置了相当多的函数,在以后我们会逐渐地碰到它们。
控制流
和常规的编程语言一样,GLSL
有一些基本的控制流结构。
条件语句if
,可以根据条件来执行不同的代码块。
if(condition){
// ...
}else if(anotherCondition){
// ...
}else{
// ...
}
这里要注意一点:Shader
是针对整个屏幕的像素进行处理的,因此if
的所有分支只要满足一定的条件,都会被执行,也就是说,某一个分支对于其中一个点无效,那对应除了这个点之外的其他点可能就有效了。
循环语句for,可以用来重复执行一段代码。
for(int i=0;i<8;i++){
// repeat ...
}
此外,还有while、do-while、switch这些常规的编程语言都支持的语句,这里就不再赘述了
变量限定符
变量限定符是用来描述变量的存储和使用方式的关键词。
它的书写格式是变量限定符 变量类型 变量名;
,语句结尾要加分号,例如:
uniform vec3 uColor;
以上代码声明了一个uniform
类型的名为uColor
的 3 维向量。
在GLSL
中,常用的变量限定符有以下几种:uniform
、const
、varying
、attribute
等。
目前我们先要了解uniform
和const
,其他的我们以后会碰到。
uniform
uniform
变量是一种全局的变量,一旦定义后会同时存在于顶点着色器与片元着色器中,并且它在每一个顶点和片元上的值都是相同的,是一个“统一”的值。
在我们目前的Shader
创作环境中,有一些内置的uniform
变量可供我们直接使用,无需显式地去声明它们。
要注意的一点是:以后在three.js
环境下,这些变量就不是内置的了,需要手动地去注入和声明。
iTime
iTime
变量表示Shader
从开始到现在执行所经过的时间。
如果要创作动画效果,这个变量可以说是必备的。
iResolution
iResolution
变量表示Shader所在画布的大小,默认是占满整个屏幕。
有时我们可能会根据屏幕的比例对Shader
的坐标进行适当的调整。
iMouse
iMouse
变量表示用户鼠标当前所在的位置。
如果要创作可交互的效果,这个变量也是必备的。
const
const
定义的量是一种常量,它是无法被改变的一个值。
比如我声明一个圆周率
const float PI=3.14159265359;
宏
宏(macros)
是一种预处理指令,用于在编译时进行文本的替换,常用于定义常量、函数、条件编译等。
宏定义的格式是#define 宏的名称 宏的值
,语句结尾没有分号
#define PI 3.14159265359
以上代码定义了一个名为PI
的宏,Shader
编译时会将所有的PI
替换为3.14159265359
这个值,可以类比为JS
的String.replace
这个函数。
宏也可以带有参数,如下所示:
#define add(a,b) a+b
以上代码定义了一个名为add
的宏,接受 2 个参数a
和b
,对它们应用相加的运算,并且无需指定明确的类型,调用这个宏时只要参数的类型相匹配,就能正确执行宏定义的运算。
宏也可以条件编译,例如:
#define IS_IN_SHADERTOY 1
#if IS_IN_SHADERTOY==1
#define iChannel0Cube iChannel0
#endif
这个宏的#if
部分会判断IS_IN_SHADERTOY
这个宏的值是否为 1
,是的话就会将下面的一行宏代码#define iChannel0Cube iChannel0
包含到Shader
代码中去,不是的话就忽略掉这行宏代码
需要注意的是,宏定义是一种文本替换机制,并没有类型检查和作用域限制,因此要根据实际的情况来使用它。