# GLSL 基础

GLSL - OpenGL Shading Language 也称作 GLslang,是一个以 C 语言为基础的高阶着色语言。 它是由 OpenGL ARB 所建立,提供开发者对绘图管线更多的直接控制,而无需使用汇编语言或硬件规格语言。

# 基本规范

  • 大小写敏感;
  • 语句必须以分号结尾;
  • 以 main()函数为主函数;
  • 单行注释以 // 开头,多行注释包含在 /* */ 中;
  • 基本数据类型:
    • float - 浮点型;
    • int - 整型;
    • ture/false - bool 类型;
float f = 1.0;
bool b = false;
  • 变量名不能以 gl*、webgl 和_webgl*开头。

# 变量

变量是用于存储信息的"容器"。

# 赋值

在对变量进行赋值时需要保证两边的数据类型一致。

int i = 8.0; // ×
int i = 8; // √

如果需要将一种类型赋值给另一种类型,可以考虑使用类型转换函数。如基础数据的转换:

  • 将整数和布尔转换为浮点 - float()
  • 将浮点和布尔转换为整数 - int()
  • 将整数和浮点转换为布尔 - bool()
int i = int(8.0);

# 作用域

可以通过函数或大括号建立块级作用域,块级作用域内建立的变量就是局部变量。局部变量只在块级作用域有效。在代码块内可以直接获取其父级定义域的变量。在局部作用域之外建立的变量,或通过 attribute、uniform、varying 限定字声明的变量都是全局变量。

# 向量

在 GLSL 支持 2、3、4 维向量,根据分量的数据类型,向量可以分为以下 3 类:

  • 浮点:vec2、vec3、vec4
  • 整数:ivec2、ivec3、ivec4
  • 布尔:bvec2、bvec3、bvec4

# 创建

由于向量在 GLSL 中使用率非常高,所以为其提供了非常灵活的使用方式,当我们需要创建一个变量时:

vec2 v0 = vec2(1.0, 1.0); // (1.0, 1.0)
vec3 v1 = vec3(v0, 0.0); // (1.0, 1.0, 0.0)
vec4 v2 = vec4(1.0); // (1.0, 1.0, 1.0, 1.0)
vec4 v3 = vec4(v0, v1); // (1.0, 1.0, 1.0, 1.0)

需要注意的是两侧的类型必须是一致的,比如下面的写法是错误的:

vec3 v0 = vec2(1.0, 1.0); // ×

# 访问方式

通过分量属性访问:

v3.x, v3.y, v3.z, v3.w  // 齐次坐标
v3.r, v3.g, v3.b, v3.a  // 色值
v3.s, v3.t, v3.p, v3.q  // 纹理坐标

将分量的多个属性连在一起访问,可以直接获得对应值组成的向量:

vec4 v4 = vec4(1.0, 2.0, 3.0, 4.0);
v4.xy // (1.0, 2.0)
v4.yx // (2.0, 1.0)
v4.xw // (1.0, 4.0)

另外,通过分量索引访问也是支持的:

vec2 v5 = vec2(v4[0], v4[1]);

类似的,我们也可以通过访问值的方式,对向量进行更新:

v4.x = 1.0;
v4[0] = 1.0;
v4.xy = vec2(1.0, 2.0);

# 运算

  • 向量和单独数字的运算:对向量中的每一个分量与数字进行运算

    向量与标量只能进行乘除,不能加减。

  • 向量和向量的运算
    • 加法
      • 数学意义:两个向量的维度必须一样,两个向量的各个维度相加,得到一个新向量;
      • 几何意义:把这个两个向量首尾相连,从起始端指向末端的一个向量。
    • 减法
      • 数学意义:两个向量的维度必须一样,两个向量的各个维度相减,得到一个新向量;
      • 几何意义:A 向量-B 向量=从 B 向量的带箭头的那一端指向 A 向量带箭头的那一端。
    • 乘法
      • 点乘
        • 从代数角度看,点积是对两个向量对应位置上的值相乘再相加的操作,其结果即为点积;
        • 从几何角度看,点积是两个向量的长度与它们夹角余弦的积。
      • 叉乘
        • 其运算结果是一个向量,并且与这两个向量都垂直,是这两个向量所在平面的法线向量。使用右手定则确定其方向。
        • a x b = (a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x)
    • 除法

      向量之间没有除法。

  • 向量和矩阵的运算
    • 行向量左乘矩阵,结果是行向量。列向量右乘矩阵,结果是列向量。另外两种组合是不允许的。

      矩阵只能与向量进行乘法运算,向量被当作是一行或一列的矩阵。

# 矩阵

在 GLSL 中的矩阵是列主序的。

# 创建

  • 浮点
mat4 m = mat4(
    1, 5, 9, 13,
    2, 6, 10, 14,
    3, 7, 11, 15,
    4, 8, 12, 16
);
  • 向量
vec4 v4_1 = vec4(1, 2, 3, 4);
vec4 v4_2 = vec4(5, 6, 7, 8);
vec4 v4_3 = vec4(9, 10, 11, 12);
vec4 v4_4 = vec4(13, 14, 15, 16);
mat4 m = mat4(v4_1, v4_2, v4_3, v4_4);
  • 组合使用浮点和向量;
  • 单个浮点数
mat4 m = mat4(1);
/*
[
    1, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
]
*/

注意:如矩阵中的参数数量大于 1,但又小于矩阵元素数量,会报错。

# 访问方式

通过一个中括号来结合整型数字来访问矩阵中的某一行:

mat4 n = mat4(
    m[0],
    m[1],
    3, 7, 11, 15,
    4, 8, 12, 16
);

注意:中括号中的数字只能是整形字面量或用 const 修饰的变量:

m[0]; // √
int i = 0;
const int j = 0;

m[i]; // ×
m[j]; // √

如果想要访问具体的某一行某一列的数据则可以使用 2 个中括号来实现,如访问第 2 行和第 1 列中的数字:

m[1][0];

事实上,从第一个中括号中取出来的数据可以视作一个向量去读取其中具体的值,如同样是访问第 2 行和第 1 列中的数字:

m[0].x;

# 运算

  • 与单个数字的运算(将其中的每一项分别和该数字进行运算)
m += 1.0;
m -= 1.0;
m *= 2.0;
m /= 2.0;
  • 矩阵与矩阵的运算
    • 矩阵加法、减法、除法:相同索引位置的元素相加相减相除
    • 矩阵乘法:
      • (M x N) 矩阵与 (N x P) 矩阵相乘,得到 (M x P) 矩阵。
      • 计算技巧:结果矩阵中的第 i 行第 j 列的元素,是矩阵 A 的第 i 行与矩阵 B 的第 j 列的元素相乘和。

# struct 结构体

结构体与构造函数类型,申明一个结构体:

struct Light{
    vec4 color;
    vec3 pos;
};

创建一个结构体:

Light l1 = Light(
    vec4(255,255,0,255),
    vec3(1,2,3)
);

访问属性:

gl_FragColor = l1.color / 255.0;

# 数组

GLSL 中的数组是典型的类型数组,它仅只支持一维数组,且不支持 push()、pop()等操作。在建立某个类型的数组时,在数据类型后面加[],[]中写数组的长度:

vec4 vs[2];
vs[0] = vec4(1, 2, 3, 4);
vs[1] = vec4(5, 6, 7, 8);

注意:指定函数的长度时可以是整形字面量或使用 const 限定字修饰的整形变量。

# 程序流程控制

可以通过流程控制语句来实现基于不同的条件执行不同的动作、按条件重复执行等。

# IF 条件判断

GLSL 中支持像 C 中的 if 写法一样,可以使用 if、else if 和 else 进行判断。

float dist = distance(gl_PointCoord,vec2(0.5,0.5));

if(dist >= 0.5){
    gl_FragColor= vec4(1.0, 0.0, 0.0, 1.0);
} else {
    discard; // 取消片元的绘制
}

注意:如果 if 语句太多会降低设色器执行速度。

# For 循环

For 循环中的变量只能有一个,并且只能是 int 或 float 类型;另外,在循环体中也可以使用 break 或 continue 语句。

for(float f = 0.0; f<=4.0; f++) {
}

# 函数

在 GLSL 中的函数声明与 C 中类似,我们也可以将函数体放到其调用方法的后面,不过在调用之前得提前声明函数。

float getLum(vec3);

// use getLum func

float getLum(vec3 color) {
  return dot(color, vec3(0.2126, 0.7162, 0.0722));
}

通过参数限定词,我们可以更好地控制参数的行为,参数行为主要是围绕参数读写和拷贝考虑的:

  • in - 参数深拷贝,可读写,不影响原始数据(默认的限定词)。
  • out - 参数浅拷贝,可读写,影响原始数据。
  • const in - 常量限定词,只读。
float getLum(const in vec3 color) {
  return dot(color, vec3(0.2126, 0.7162, 0.0722));
}
  • inout - 功能类似于 out,用得不多,知道即可。

注:GLSL ES 还有许多内置方法:官方文档 (opens new window)

# 其它

# 精度限定词

精度限定词可以提高着色程序的运行效率,削减内存开支。精度类型主要包含:

  • highp 高精度
  • mediump 中精度
  • lowp 低精度

中精度用得比较多,因为高精度太耗性能,而且有时候片元着色器会不支持。我们可以为某个变量设置精度:

mediump float size;
highp vec4 position;
lowp vec4 color;

也可以为某种数据类型设置精度:

precision mediump float;
precision highp int;

注意:着色器中,除了片元着色器的 float 数据没默认精度,其余的数据都是有默认精度的。因此,我们在片元着色器里要提前声明好浮点型数据的精度。

# 参考