Skip to content

简介

GLSL 是用于编写 着色器(Shader),适合进行图形计算的语言,它包含一些针对向量和矩阵操作的特性,使渲染管线具有可编程性且运行高效。本章主要介绍在编写着色器时常用的一些语法。

变量

变量及变量类型

变量类型说明符描述
bool布尔型标量数据类型。
int / ivec2 / ivec3 / ivec4包含 1 / 2 / 3 / 4 个整型的向量。
float / vec2 / vec3 / vec4包含 1 / 2 / 3 / 4 个浮点型的向量。
half / half2 / half3 / half4包含 1 / 2 / 3 / 4 个 32 位浮点型的向量。
sampler2D表示 2D 纹理。
mat2 / mat3 / mat4表示 2\*2 / 3\*3 / 4\*4 的矩阵。

标量

GLSL 构造标量的方式为定义变量的类型,然后定义变量的命名,最后定义变量的初始值。以下为定义标量的案例:

c++
bool isTrue = true;
int a = 1;
float b = 0.1;

向量

构造向量时的规则如下:

  • 若向向量构造器提供了一个标量,则向量的所有值都会设定为该标量值。
c++
vec4 _Vec4 = vec4(1.0);              
// _Vec4 = {1.0, 1.0, 1.0, 1.0};
  • 若提供多个标量值或向量,则从左到右使用提供的值赋值。前提是标量或向量的数量之和要等于向量所含分量的数量。
c++
vec2 _Vec2 = vec2(2.0,2.0)
vec4 _Vec4 = vec4(_Vec2,1.0,1.0);              
// _Vec4 = {2.0, 2.0, 1.0, 1.0};

获取向量不同分量的特性如下:

  • 您可以依照 r,g,b,a 关键字提取四个不同分量的标量值,也可以依照 x,y,z,w 关键字提取,两种提取和定义的方式相同。因为在着色器编码时向量可以用于表示位置也可以表示颜色值。
c++
vec3 _Vec3 = vec3(1.0,2.0,3.0)
float _X = _Vec.x;             
// _X = 1.0;
float _R = _Vec.r;             
// _R = 1.0;
  • 您可以依照 r,g,b,a 关键字或依照 x,y,z,w 关键字同时通过访问多个位置的标量值。值得注意的是,不能同时依照 r,g,b,a 关键字和依照 x,y,z,w 关键字访问。
c++
vec4 _Vec4 = vec4(1.0,2.0,3.0,4.0)
vec3 _Vec3_0 = _Vec4.xyz;
// _Vec3_0 = {1.0,2.0,3.0};
vec3 _Vec3_1 = _Vec4.rgb;
// _Vec3_1 = {1.0,2.0,3.0};
vec3 _Vec3_2 = _Vec4.zzx;
// _Vec3_2 = {3.0,3.0,1.0};

矩阵

您在 GLSL 内可使用 mat2 / mat3 / mat4 来表示 2 阶到 4 阶的矩阵。

GLSL 中是列矩阵存储,因此构造时,构造矩阵时会按照列顺序进行填充,矩阵构造有如下的规则:

  • 矩阵可以由单个标量从左向右进行构造。
c++
mat2 _Marixt2x2 = mat2(
  1.0,2.0,               //第一列;
  3.0,4.0                //第二列;
);
  • 若只为矩阵构造器提供了一个标量,则该标量值会构造矩阵对角线上的值。
c++
mat4 _Marixt4x4 = mat4(1.0); 
/*
  _Marixt4x4 = {
    1.0, 0.0, 0.0, 0.0, 
    0.0, 1.0, 0.0, 0.0 
    0.0, 0.0, 1.0, 0.0                    
    0.0, 0.0, 0.0, 1.0 
  };
*/
  • 矩阵可以由多个向量构造。
c++
vec2 _Vec2_0 = vec2(1.0, 2.0);
vec2 _Vec2_1 = vec2(3.0, 4.0);

mat2 matrix2x2 = mat2(col1, col2);
/*
  _Marixt2x2 = {
    1.0, 2.0,
    3.0, 4.0,
  };
*/

矩阵获取或访问某个数据有如下规则:

  • 矩阵可以通过第 1 个索引获取矩阵的某一列的数据,用第 2 个索引获取该列特定位置标量值。值得注意的是,索引是从 0 开始。
c++
mat2 _Marixt2x2 = mat2(
  1.0,2.0,               //第一列;
  3.0,4.0                //第二列;
); 

vec2 _Vec2_0 = _Marixt2x2[0];
// _Vec2_0 = {1.0,2.0};

float _f01 = _Marixt2x2[0][1];
// _f01 = 1.0;
  • 矩阵的索引必须为常量索引,常量索引的定义如下:

    • 整形数字。

    • 用 const 限定符定义的全局变量或局部变量,不包括方法参数。

    • 循环索引。

    • 由前述三条中的项组成的表达式。

c++
mat2 _Marixt2x2 = mat2(
  1.0,2.0,               //第一列;
  3.0,4.0                //第二列;
); 

int _i = 1;

float _f0i = _Marixt2x2[0][_i];
// _f01 = 1.0;

结构体

结构体通过 struct 关键字声明结构。结构体可以组合基本类型和数组来形成您自定义的新类型,该新类型可由不同数据类型聚合而成:

c++
struct v2f
{
    vec4 pos;
    vec4 color;
    vec2 uv;
};

构造结构体的代码示例如下:

c++
v2f vert()
{
    v2f v;
    v.pos = vec4(0.0, 0.0,0.0,0.0);
    v.color = vec4(1.0, 1.0, 1.0, 1.0);
    v.uv = vec2(0.5, 0.5);

    return v;
}

结构体之间支持通过赋值(=)和 比较(== 和 !=)运算符联系,但要求两个结构体拥有相同的类型。

数组

数组的定义和初始化规则如下:

  • 数组必须定义长度。

  • 数组不能在定义的同时初始化。

  • 数组必须用常量表达式初始化。

  • 数组不能用 const 限定符定义数组。

  • 不支持多维数组。

数组声明和初始化的代码示例如下:

c++
float _Array[4];
for(int i =0; i < 4; i ++)
{
    _Array[i] = 0.0;
}

限定符

储存限定符

储存限定符用于限定变量在管线中的作用。

储存限定符描述
< none:default >无限定符或者使用 default 限定符,常用于修饰局部变量、方法参数。
const限定变量编译时为常量或作为只读参数。值得注意的是,变量需要在声明时完成初始化,该变量值只读,不可修改。
attribute限定变量为只读的顶点数据,仅用在 顶点着色器(Vertex Shader)中,用于程序和顶点着色器间通信,确定顶点格式、信息。
varying顶点着色器(Vertex Shader)传输给 片元着色器(Fragment Shader)的插值。

用于修饰方法参数的限定符:

储存限定符描述
< none:in >限定这个参数仅用于传入方法中,且可在方法中改变该参数的值。其为方法参数的默认限定符。
out限定这个参数的值不会传入方法中,由方法内部修改并返回修改后的值。
inout相当于 in 和 out 的结合体,定义的参数既能传入方法中,又能在方法内部修改后并返回修改后的值。

精度限定符

GLSL 引入了精度限定符,用于限定 整型(int)或 浮点型(float)变量的精度。在 着色器(Shader)中如果没有限定精度,则所有的整型和浮点型变量都采用高精度计算。

GLSL 支持的精度限定符包括以下种类:

变量类型说明符描述
< none:highp >无限定符或者使用 highp 高精度限定符。 浮点型精度范围为 -262 至 262。 整型精度范围为 -216 至 216
mediump中精度。 浮点型精度范围为 -214 至 214。 整型精度范围为 -210 至 210
lowp低精度。 浮点型精度范围为 -28 至 28。 整型精度范围为 -28 至 28

控制流

条件流程

GLSL 的条件流程大体上是标准的 C 和 C++ 的条件流程,包括:

  • 条件语句 if-else / switch-case

  • 循环语句 for / while / do-while

  • 跳出循环语句 return / break / continue

值得注意的是 GLSL 没有 goto,您可以使用 discard 代替该语句的逻辑。该语句仅在 片元着色器(Fragment Shader)下有效,需要注意的是使用该语句会导致管线放弃当前 片元(Fragment),不会写入帧缓存。

在 GLSL 中,循环变量必须是常量或者编译时已知的变量,代码示例如下:

c++
//示例中的变量 _MaxV 就是用 const 限定符定义的常量;
const float _MaxV = 5.0; 
for(float i = 0.0; i < _MaxV; i ++){ 
   //这里编辑循环中的逻辑;
}

方法

GLSL 中 方法(Function)由 返回值(Return)、方法名(Function Name)和 参数(Parameters)构成,其中定义方法必须有返回值和方法名。

  • 若无返回值,需要使用 void 代替。

  • 若有返回值,方法中必须用 return 字段定义该类型的返回值。

代码示例如下:

c++
/*
定义方法:
  *方法没有返回值;
  *方法名为 dec;
  *传入的参数是 vec4 类型,并在方法中命名为 sPos;
*/
void dec(inout float a)
{
  a = a + 0.1;
}

/*
定义方法:
  *返回值是 vec4 类型;
  *方法名为 vert;
  *传入的参数是 vec4 类型,并在方法中命名为 sPos;
*/
vec4 vert(vec4 sPos)
{
    vec4 cPos;
    cPos = vec4(sPos.xyz, 1.0);
    return cPos;
}

定义预处理宏

宏(Macros)是一种预处理指令,用于在编译时进行指令的替换,常用于定义常量、函数、条件编译等。

着色器(Shader)通过预处理宏定义多样化的动态分支,以低性能损耗确定最终的渲染效果。

在 GLSL 中常使用以下指令定义预处理宏:

指令描述
#define定义一个预处理宏。格式是 #define Macros_Name Macros_Value 。
#undef取消宏的定义。格式是 #undef Macros_Name 。
#if编译预处理中的条件。
#ifdef判断某个宏是否被定义,若已定义,执行随后的语句。
#ifndef与 #ifdef 相反,判断某个宏是否未被定义。
defined与 #if#elif 配合使用,判断某个宏是否被定义。 格式是 #if defined(Macros_Name) 或者 #elif defined(Macros_Name)
#elif若 #if 或前面的 #elif 条件均不满足,则执行 #elif 之后的条件判断,相当于 else if。
#else与 #if#ifdef#ifndef 对应,若这些条件不满足,则执行 #else 之后的逻辑。
#endif#if#ifdef#ifndef 这些条件命令的结束标志。

详细内容

关于 GLSL 详细的使用方法和语法解释,请参阅 OpenGL 官方文档:

https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html