OpenGL学习(十)-- 着色语言 GLSL 语法介绍
我的
OpenGL
专题学习目录,希望和大家一起学习交流进步!
- OpenGL学习(一)-- 术语了解
- OpenGL学习(二)-- Xcode 搭建 OpenGL 环境
- OpenGL学习(三)-- OpenGL 基础渲染
- OpenGL学习(四)-- 正面&背面剔除和深度测试
- OpenGL学习(五)-- 裁剪与混合
- OpenGL学习(六)-- 基础纹理
- OpenGL学习(七)-- 基础变化综合练习实践总结
- OpenGL学习(八)-- OpenGL ES 初探(上)
- OpenGL学习(九)-- OpenGL ES 初探(下)GLKit
- OpenGL学习(十)-- 着色语言 GLSL 语法介绍
- OpenGL学习(十一)-- 用 GLSL 实现加载图片
- OpenGL学习(十二)-- OpenGL ES 纹理翻转的策略对比
一、简介
GLSL
(OpenGL Shading Language) 全称 OpenGL 着色语言,是用来在 OpenGL 中着色编程的语言,也即开发人员写的短小的自定义程序,他们是在图形卡的 GPU上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。 GLSL
其使用 C 语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。
二、变量命名
GLSL
的变量命名方式与 C 语言类似,可使用字母,数字以及下划线,不能以数字开头。还需要注意的是,变量名不能以 gl_
作为前缀,这个是 GLSL
保留的前缀,用于 GLSL
的内部变量。
三、数据类型
1、基本数据类型
类型 | 描述 |
---|---|
void | 跟 C 语言的 void 类似,表示空类型。作为函数的返回类型,表示这个函数不返回值。 |
bool | 布尔类型,true 或 false,以及可以产生布尔型的表达式。 |
int | 有符号整型 |
uint | 无符号整形 |
float | 浮点型 |
2、特殊类型--纹理采样类型
类型 | 描述 |
---|---|
sampler1D | 用于内建的纹理函数中引用指定的 1D纹理的句柄。只可以作为一致变量或者函数参数使用 |
sampler2D | 二维纹理句柄 |
sampler3D | 三维纹理句柄 |
samplerCube | cube map 纹理句柄 |
sampler1DShadow | 一维深度纹理句柄 |
sampler2DShadow | 二维深度纹理句柄 |
3、聚合类型(向量和矩阵类型)
(1)向量类型
类型 | 描述 |
---|---|
vec2,vec3,vec4 | 2分量、3分量和4分量浮点向量 |
ivec2,ivec3,ivec4 | 2分量、3分量和4分量整数向量 |
uvec2,uvec3,uvec4 | 2分量、3分量和4分量无符号整数向量 |
bvec2,vbec3,bvec4 | 2分量、3分量和4分量布尔向量 |
A、向量声明--4分量的float 类型向量
vec4 V1;
复制代码
B、声明向量并对其进行构造
vec4 V2 = vec4(1,2,3,4);
复制代码
C、向量运算
vec4 v;
vec4 vOldPos = vec4(1,2,3,4);
vec4 vOffset = vec4(1,2,3,4);
//注意:接下来假设所有参与运算的变量已定义并赋值。
v = vOldPos + vOffset;
v = vNewPos;
v += vec4(10,10,10,10);
v = vOldPos * vOffset;
v *= 5;
复制代码
D、向量元素的获取(成分选择)
向量中单独的成分可以通过 {x,y,z,w}, {r,g,b,a} 或者 {s,t,p,q} 的记法来表示。这些不同的记法用于 顶点,颜色,纹理坐标。在成分选择中,你不可以混合使用这些记法。其中 {s,t,p,q} 中的 p 替换了纹理的 r 坐标,因为与颜色 r 重复了。下面是用法举例: 例如有向量 v1 和 v2:
vec3 v1 = {0.5, 0.35, 0.7};
vec4 v2 = {0.1, 0.2, 0.3, 0.4};
复制代码
可以通过 {x,y,z,w}, {r,g,b,a} 或者 {s,t,p,q} 来取出向量中的元素值。 通过 x,y,z,w:
v2.x = 3.0f;
v2.xy = vec2(3.0f,4.0f);
v2.xyz = vec3(3,0f,4,0f,5.0f);
复制代码
通过 r,g,b,a:
v2.r = 3.0f;
v2.rgba = vec4(1.0f,1.0f,1.0f,1.0f);
复制代码
通过 s,t,q,r:
v2.stqr = vec2(1.0f, 0.0f, 0.0f, 1.0f);
复制代码
错误示例:
float myQ = v1.q;// 出错,数组越界访问,q代表第四个元素
float myRY = v1.ry; // 不合法,混合使用记法
复制代码
向量还支持一次性对所有分量操作
v1.x = v2.x +5.0f;
v1.y = v2.y +4.0f;
v1.z = v2.z +3.0f;
v1.xyz = v2.xyz + vec3(5.0f,4.0f,3.0f);
复制代码
(2)矩阵类型
类型 | 描述 |
---|---|
mat2 或 mat2x2 | 2x2的浮点数矩阵类型 |
mat3 或 mat3x3 | 3x3的浮点数矩阵类型 |
mat4 或 mat4x4 | 4x4的浮点数矩阵类型 |
mat2x3 | 2列3行的浮点矩阵(OpenGL的矩阵是列主顺序的) |
mat2x4 | 2列4行的浮点矩阵 |
mat3x2 | 3列2行的浮点矩阵 |
mat3x4 | 3列4行的浮点矩阵 |
mat4x2 | 4列2行的浮点矩阵 |
mat4x3 | 4列3行的浮点矩阵 |
创建矩阵:
mat4 m1,m2,m3;
复制代码
构造单元矩阵:
mat4 m2 = mat4(1.0f,0.0f,0.0f,0.0f
0.0f,1.0f,0.0f,0.0f,
0.0f,0.0f,1.0f,0.0f,
0.0f,0.0f,0.0f,1.0f);
复制代码
或者
mat4 m4 = mat4(1.0f);
复制代码
4、数组
GLSL
中只可以使用一维的数组。数组的类型可以是一切基本类型或者结构体。下面的几种数组声明是合法的:
float floatArray[4];
vec4 vecArray[2];
float a[4] = float[](1.0,2.0,3.0,4.0);
vec2 c[2] = vec2[2](vec2(1.0,2.0),vec2(3.0,4.0));
复制代码
数组类型内建了一个length()
函数,可以返回数组的长度。
lightPositions.length() // 返回数组的长度
复制代码
5、结构体
结构体可以组合基本类型和数组来形成用户自定义的类型。在定义一个结构体的同时,你可以定义一个结构体实例。或者后面再定义。
struct fogStruct {
vec4 color;
float start;
float end;
vec3 points[3]; // 固定大小的数组是合法的
} fogVar;
复制代码
可以通过 = 为结构体赋值,或者使用 ==,!= 来判断两个结构体是否相等。
fogVar = fogStruct(vec4(1.0,0.0,0.0,1.0),0.5,2.0);
vec4 color = fogVar.color;
float start = fogVar.start;
复制代码
三、修饰符
1、变量存储限定符
限定符 | 描述 |
---|---|
(默认的可省略)只是普通的本地变量,可读可写,外部不可见,外部不可访问 | |
const | 常量值必须在声明时初始化,它是只读的不可修改的 |
varying | 顶点着色器的输出,主要负责在 vertex 和 fragment 之间传递变量。例如颜色或者纹理坐标,(插值后的数据)作为片段着色器的只读输入数据。必须是全局范围声明的全局变量。可以是浮点数类型的标量,向量,矩阵。不能是数组或者结构体。 |
uniform | 一致变量。在着色器执行期间一致变量的值是不变的。与 const 常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。一致变量在顶点着色器和片段着色器之间是共享的。它也只能在全局范围进行声明。 |
attribute | 表示只读的顶点数据,只用在顶点着色器中。数据来自当前的顶点状态或者顶点数组。它必须是全局范围声明的,不能再函数内部。一个 attribute 可以是浮点数类型的标量,向量,或者矩阵。不可以是数组或则结构体 |
centorid varying | 在没有多重采样的情况下,与 varying 是一样的意思。在多重采样时,centorid varying 在光栅化的图形内部进行求值而不是在片段中心的固定位置求值。 |
invariant | (不变量)用于表示顶点着色器的输出和任何匹配片段着色器的输入,在不同的着色器中计算产生的值必须是一致的。所有的数据流和控制流,写入一个 invariant 变量的是一致的。编译器为了保证结果是完全一致的,需要放弃那些可能会导致不一致值的潜在的优化。除非必要,不要使用这个修饰符。在多通道渲染中避免 z-fighting 可能会使用到。 |
2、函数参数限定符
GLSL
允许自定义函数,但参数默认是以值形式(in
限定符)传入的,也就是说任何变量在传入时都会被拷贝一份,若想以引用方式传参,需要增加函数参数限定符。
限定符 | 描述 |
---|---|
in | 用在函数的参数中,表示这个参数是输入的,在函数中改变这个值,并不会影响对调用的函数产生副作用。(相当于C语言的传值),这个是函数参数默认的修饰符 |
out | 用在函数的参数中,表示该参数是输出参数,值是会改变的。 |
inout | 用在函数的参数,表示这个参数即是输入参数也是输出参数。 |
其中使用 inout 方式传递的参数便与其他 OOP 语言中的引用传递类似,参数可读写,函数内对参数的修改会影响到传入参数本身。 eg:
vec4 getPosition(out vec4 p){
p = vec4(0.,0.,0.,1.);
return v4;
}
void doubleSize(inout float size){
size= size * 3.0 ;
}
复制代码
三、GLSL 中的运算
⚠️注意 GLSL
中没有隐式转换,即便在多维向量中也没有,类似下面这样的的赋值都是错误的:
// ⚠️错误
int a = 2.0;
vec4 v4=vec4(1.0, 1.0, 2, 1.0);
复制代码
1、不同类型 float 与 int 间的运算:
float 与 int 之间进行运算,需要进行一次显示转换,以下表达式都是正确的:
int a = int(2.0);
float a = float(2);
int a = int(2.0)*2 + 1;
float a = float(2)*6.0+2.3;
复制代码
2、float 与 vec(向量)、mat(矩阵) 的运算:
- 逐分量运算 vec,mat 这些类型其实是由 float 复合而成的,当它们与float 运算时,其实就是在每一个分量上分别与 float 进行运算,这就是所谓的 逐分量运算。GLSL 里,大部分涉及 vec,mat 的运算都是逐分量运算,但也并不全是。下文中就会讲到特例。 逐分量运算 是线性的,这就是说 vec 与 float 的运算结果是还是 vec。
int 与 vec,mat 之间是不可运算的,因为 vec 和 mat 中的每一个分量都是 float 类型的,无法与 int 进行逐分量计算。
下面枚举了几种 float 与 vec,mat 运算的情况:
vec3 a = vec3(1.0, 2.0, 3.0);
mat3 m = mat3(1.0);
float s = 10.0;
vec3 b = s * a; // vec3(10.0, 20.0, 30.0)
vec3 c = a * s; // vec3(10.0, 20.0, 30.0)
mat3 m2 = s * m; // = mat3(10.0)
mat3 m3 = m * s; // = mat3(10.0)
复制代码
3、vec(向量) 与 vec(向量)运算:
两向量间的运算首先要保证操作数的阶数都相同,否则不能计算。例如: vec3*vec2
和 vec4+vec3
等等都是不行的。
它们的计算方式是两操作数在同位置上的分量分别进行运算,其本质还是逐分量进行的,这和上面所说的 float 类型的逐分量运算可能有一点点差异,相同的是 vec 与 vec 运算结果还是 vec,且阶数不变。
vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(0.1, 0.2, 0.3);
vec3 c = a + b; // = vec3(1.1, 2.2, 3.3);
vec3 d = a * b; // = vec3(0.1, 0.4, 0.9);
复制代码
4、vec(向量) 与 mat(矩阵):
要保证操作数的阶数相同,且 vec 与 mat 间只存在乘法运算。 它们的计算方式和线性代数中的矩阵乘法相同,不是逐分量运算。
vec2 v = vec2(10., 20.);
mat2 m = mat2(1., 2., 3., 4.);
vec2 w = m * v; // = vec2(1. * 10. + 3. * 20., 2. * 10. + 4. * 20.)
vec2 v = vec2(10., 20.);
mat2 m = mat2(1., 2., 3., 4.);
vec2 w = v * m; // = vec2(1. * 10. + 2. * 20., 3. * 10. + 4. * 20.)
复制代码
5、mat(矩阵) 与 mat(矩阵):
⚠️要保证操作数的阶数相同。
在 mat 与 mat 的运算中,除了乘法是线性代数中的矩阵乘法外,其余的运算仍为逐分量运算。简单说就是只有乘法是特殊的,其余都和 vec 与 vec 运算类似。
mat2 a = mat2(1., 2., 3., 4.);
mat2 b = mat2(10., 20., 30., 40.);
mat2 c = a * b; // mat2(1.*10.+3.*20.,2.*10.+4.*20.,1.* 30.+3.*40.,2.* 30.+4.*40.);
mat2 d = a+b;// mat2(1.+10.,2.+20.,3.+30.,4.+40);
复制代码
四、类型转换
GLSL
可以通过构造函数进行显式转换,方法如下:
bool t= true;
bool f = false;
int a = int(t); // true转换为1或1.0
int a1 = int(f);// false转换为0或0.0
float b = float(t);
float b1 = float(f);
bool c = bool(0);// 0或0.0转换为false
bool c1 = bool(1);// 非0转换为true
bool d = bool(0.0);
bool d1 = bool(1.0);
复制代码
五、精度限定
GLSL
在进行光栅化着色的时候,会产生大量的浮点数运算,这些运算可能是当前设备所不能承受的,所以 GLSL
提供了 3 种浮点数精度,我们可以根据不同的设备来使用合适的精度。 在变量前面加上 highp
、mediump
、lowp
即可完成对该变量的精度声明:
lowp float color;
varying mediump vec2 Coord;
lowp ivec2 foo(lowp mat3);
highp mat4 m;
复制代码
我们一般在 片元着色器(fragment shader) 最开始的地方加上 precision mediump float;
便设定了默认的精度,这样所有没有显式表明精度的变量都会按照设定好的默认精度来处理。
六、控制语句
在语法上,GLSL
与 C 非常相似, 也有 if else、for、while、do while
,使用 continue
跳入下一次循环,break
结束循环。
for (l = 0; l < numLights; l++) {
if (!lightExists[l]);
continue;
color += light[l];
}
while (i < num) {
sum += color[i];
i++;
}
do {
color += light[lightNum];
lightNum--;
} while (lightNum > 0)
复制代码
除了这些,GLSL
还多了一种特殊的控制语句 discard
,它会立即跳出片元着色器,并不在向下任何语句。也就不执行后面的片段着色操作,片段也不会写入帧缓冲区。
if (true)
discard;
复制代码
⚠️注意 GLSL
函数中没有递归!
以上的总结参考了并部分摘抄了以下文章,非常感谢以下作者的分享!: