0%

02_着色器基础

1. 着色器简介

1.1 OpenGL 的可编程管线

  • 顶点着色阶段
     
  • 细分着色阶段
    • 细分控制着色器
    • 积分赋值着色器
       
  • 几何着色阶段
     
  • 片元着色阶段
     
  • 计算着色阶段

1.2 着色器

着色器类似一个函数调用的方式:数据传输进来,经过处理,然后再传输出去。

GLSL 的 main() 函数没有任何参数,在某个着色阶段中输入和输出的所有数据都是通过着色器中的特殊全局变量来传递的

2. GLSL 概述

2.1 使用 GLSL 构建着色器

2.1.1 着色器基本结构

1
2
3
4
5
6
7
#version 400 core

void
main()
{
// 在这里编写代码
}

2.1.2 变量的声明

GLSL的基本类型

数据类型 描述
void 跟C语言的void类似,表示空类型。作为函数的返回类型,表示这个函数不返回值。
bool 布尔类型,可以是 true 和 false,以及可以产生布尔型的表达式。
int 整型,有符号二进制补码的 32 位整数
uint 无符号的 32 位整数
float 32 位浮点型
double 64 位浮点型
sampler1D 用于内建的纹理函数中引用指定的1D纹理的句柄。只可以作为一致变量或者函数参数使用
sampler2D 二维纹理句柄
sampler3D 三维纹理句柄
samplerCube cube map纹理句柄
sampler1DShadow 一维深度纹理句柄
sampler2DShadow 二维深度纹理句柄

2.1.3 变量的初始化

1
2
3
4
int numParticles = 1500;
float force = -9.8;
bool falling = true;
double pi = 3.1415926;

Note

  • 整型字面量常数可以是八进制,十进制或者十六进制的值
  • 可以在末尾添加 “u” 或者 “U” 表示一个无符号的整数
     
  • 浮点字面量必须包含一个小数点,或者用科学计数法表示,例如 3E-7
  • 可以在末尾添加 “f” 或者 “F” 表示浮点数
  • 可以在末尾添加 “lF” 或者 “LF” 表示 double 精度的浮点数

2.1.4 类型转换

GLSL 的隐式类型转换

转换后 转换前
unit int
float int、unit
double int、unit、float

显示转换:

1
2
float f = 10.0;
int ten = int(f);

2.1.5 聚合类型

GLSL 的向量与矩阵类型

基本类型 2D 向量 3D 向量 4D 向量 矩阵类型
float vec2 vec3 vec4 mat2、mat3、mat4
mat2x2、mat2x3、mat2x4
mat3x2、mat3x3、mat3x4
mat4x2、mat4x3、mat4x4
double dvec2 dvec3 dvec4 dmat2、dmat3、dmat4
dmat2x2、dmat2x3、dmat2x4
dmat3x2、dmat3x3、dmat3x4
dmat4x2、dmat4x3、dmat4x4
int ivec2 ivec3 ivec4
uint uvec2 uvec3 uvec4
bool bvec2 bvec3 bvec4

初始化

1
vec3 velocity = vec3(0.0, 0.2, 0.3);

等价转换

1
vec3 step = ivec3(velocity);

降维转换

1
2
3
vec4 color;
// 现在 RGB 只有 color 的前三个分量
vec3 RGB = vec3(color);

升维转换

1
2
vec3 white = vec3(1.0);     // white = (1.0, 1.0, 1.0);
vec4 translucent = vec4(white, 0.5);

初始化对角矩阵

1
mat3 m = mat3(4.0);

m=mat3(4.0)=(4.00.00.00.04.00.00.00.04.0)m = mat3(4.0) = \begin{pmatrix} 4.0 & 0.0 & 0.0 \\ 0.0 & 4.0 & 0.0 \\ 0.0 & 0.0 & 4.0 \\ \end{pmatrix}

初始化一般矩阵

1
2
3
4
// 表示方式与实际矩阵是转置关系
mat3 M = mat3(1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
7.0, 8.0, 9.0);
1
2
3
4
5
// 使用列向量初始化
vec3 column1 = vec3(1.0, 2.0, 3.0);
vec3 column2 = vec3(4.0, 5.0, 6.0);
vec3 column3 = vec3(7.0, 8.0, 9.0);
mat3 M = mat(column1, column2, column3);
1
2
3
4
5
6
7
8
9
// 使用低维向量初始化
vec2 column1 = vec2(1.0, 2.0);
vec2 column2 = vec2(4.0, 5.0);
vec2 column3 = vec2(7.0, 8.0);

mat3 M = mat3(column1, 3.0,
column2, 6.0,
column3, 9.0);

M=(1.04.07.02.05.08.03.06.09.0)M = \begin{pmatrix} 1.0 & 4.0 & 7.0 \\ 2.0 & 5.0 & 8.0 \\ 3.0 & 6.0 & 9.0 \\ \end{pmatrix}

2.1.6 访问向量和矩阵中的元素

数组访问形式

1
2
float red = color[0];
float v_y = velocity[1];

通过向量分量名称进行访问

分量访问符 符号描述
(x,y,z,w) 与位置相关的分量
(r,g,b,a) 与颜色相关的分量
(s,t,p,q) 与纹理坐标相关的分量
1
2
float red = color.r;
float v_y = velocity.y;

swizzle 应用

1
2
3
4
5
// 基于红色通道设置亮度值
vec3 luminance = color.rrr;

// 反转 color 的每个分量
color = color.abgr;
  • 错误的访问方式:
1
2
3
4
5
6
// 错误:"z" 来自不同的访问符组合
vec4 color = otherColor.rgz;

// 错误:2D 向量不存在 "z" 分量
vec2 pos;
float zPos = pos.z;

矩阵访问

1
2
3
4
5
6
7
8
mat4 m = mat4(2.0);

// 访问矩阵的第 2 列
vec4 zVec = m[2];

// 访问矩阵的第 1 列第 1 个元素
float yScale01 = m[1][1];
float yScale02 = m[1].y;

2.1.7 结构体

同 C

1
2
3
4
5
6
7
typedef struct Particle {
float lifetime;
vec3 position;
vec3 velocity;
} Particle;

Particle p = Particle(10.0, pos, vel);

2.1.8 数组

可以定义任意类型的数组,包括结构体数组

数组的定义

1
2
3
4
5
6
// 定义有 3 个 float 元素的数组
float coeff[3];
float[3] coeff;

// 未定义维数,稍后可重新声明维数
int indices[];

数组的初始化

1
2
// 使用构造函数初始化
float coeff[3] = float[3](2.38, 3.14, 42.0);

访问数组长度

1
2
3
4
5
6
7
8
int len = coeff.length();

//矩阵
mat3x4 m;
// m 包含的列向量个数为 3
int c = m.length();
// 第 0 个列向量的分量个数为 4
int r = m[0].length();

多维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 二维数组个数为3,分量为分量个数为 5 的数组
float coeff[3][5];

// 第 2 列数组的第 1 个分量乘以 2
coeff[2][1] *= 2.0;

// 3
int len_i = coeff.length();

// 大小为 5 的一维数组
float[5] myVec5 = coeff[2];

// 5
coeff[2].length();

2.2 存储限制符

GLSL 的类型修饰符

类型修饰符 描述
const 将一个变址定义为只读形式。如果它初始化时用的是一个编译时常量,那么它本身也会成为编译时常量
in 设置这个变量为着色器阶段的输入变量
out 设置这个变量为着色器阶段的输出变量
uniform 设置这个变量为用户应用程序传递给着色器的数据,它对于给定的图元言是一个常量
buffer 设置应用程序共享的一块可读写的内存。这块内存也作为着色器中的存储缓存(storage buffer)使用
shared 设置变量是本地工作组(local work group)中共享的。它只能用于计算着色器中
varying 顶点着色器的输出。例如颜色或者纹理坐标,(插值后的数据)作为片段着色器的只读输入数据。必须是全局范围声明的全局变量。可以是浮点数类型的标量,向量,矩阵。不能是数组或者结构体。
centorid varying 在没有多重采样的情况下,与varying是一样的意思。在多重采样时,centorid varying在光栅化的图形内部进行求值而不是在片段中心的固定位置求值。

2.2.1 in 存储限制符

in 修饰符用于定义着色器阶段的输入变量。

  • 顶点着色器:顶点属性
  • 其他着色器:前一个着色器阶段的输出变量
  • 片元着色器也可以使用一些其他的关键词来限定自己的输入变量

2.2.2 out 存储限制符

out 修饰符用于定义一个着色器阶段的输出变量。

  • 顶点着色器:输出变换后的齐次坐标
  • 片元着色器:输出的最终片元颜色

2.2.3 uniform 存储限制符

在着色器运行之前, uniform 修饰符可以指定一个在应用程序中设置好的变量,它不会在图元处理的过程中发生变化。

  • uniform变量在所有可用的着色阶段之间都是共享的,它必须定义为全局变量。
  • 任何类型的变量(包括结构体和数组)都可以设置为uniform变量。
  • 着色器无法写入到 uniform 变量,也无法改变它的值。

举例来说,我们可能需要设置一个给图元着色的颜色值。此时可以声明一个 uniform 变量,将颜色值信息传递到着色器当中。而着色器中会进行如下声明:

1
uniform vec4 BaseColor;

在着色器中 ,可以根据名字 BaseColor 来引用这个变量

设置 uniform 变量

GLSL编译器会在链接着色器程序时创建一个 uniform 变量列表。

如果需要设置应用程序中BaseColor的值,我们需要首先获得

1
2
3
4
5
6
7
8
9
10
11
// 着色器中的 uniform float time 的索引
GLint timeLoc;
// 程序运行的时间
GLfloat timeValue;

// 根据变量名称获取索引
timeLoc = glGetUniformLocation(program, "time");

// 根据获得的索引对着色器中的 time 进行设置
// 相当于 time = timeLoc
glUniformlf(timeLoc, timeValue);

2.3 函数

1
2
3
4
5
6
7
8
9
returnType functionName([accessModifier] type01 variable01,
[accessModifier] type02 variable02,
...)
{
// 函数体

// 若 returnType 为 void,则不需要 return 语句
return returnValue;
}

GLSL 函数参数的访问修饰符

访问修饰符 描述
in 将数据拷贝到函数中,相当于值传递(如果没有指定修饰符,默认这种形式)
const in 将只读数据拷贝到函数中
out 从函数中获取数值(因此输入函数的值是未定义的)
inout 将数据拷贝到函数中,并且返回函数中修改的数据
  • 如果一个变量没有包含任何访问修饰器,那么参数的声明会默认设置为使用 in 修饰符;
  • 如果变量的值需要从函数中拷贝出来,那么我们就必须设置
    它为 out (只能写出的变量)或者 inout (可以读入也可以写出的变量)修饰符。

2.4 计算的不变性

GLSL无法保证在不同的着色器中,两个完全相同的计算式会得到完全一样的结果:

1
2
3
4
5
6
7
8
9
uniform float ten;             //假设应用程序设置这个值为10.0
const float f = sin(10.0); ; //宿主机的编译器负责计算
£loat g = sin(ten); //图形硬件负责计算

void main()
{
if (f == g) // f 和 g 不一定相等
;
}

2.4.1 invariant 限制符

invariant 限制符可以设置任何着色器的输出变量

  • 它可以确保如果两个着色器的输出变量使用了同样的表达式;
  • 表达式中的变量也是相同值;
  • 计算产生的结果也是相同的。
1
2
3
4
5
// 将一个内置的输出变量声明为 invariant
invariant gl_Position;

// 声明一个用户自定义的变量为 invariant
invariant centroid out vec3 Color;

在调试过程中,可能需要将着色器中的所有可变量都设置为 invariant 。

1
#pragma STDGL invariant(all)

2.4.2 precise 限制符

precise 限制符可以设置内置变量、用户变量,或者函数的返回值。

1
2
3
precise gl_Position;
precise out vec3 Location;
precise vec3 subdivide(vec3 Pl, vec3 P2) { ... }

2.5 预处理器

2.5.1 预处理器命令

预处理器命令 描述
#define
#undef
控制常量与宏的定义,与C语言的预处理器命令类似
#if
#ifdef
#ifhdef
#else
#elif
#endif
代码的条件编译,与C语言的预处理器命令和def ined操作符均类似。
条件表达式中只可以使用整数表达式或者#define 定义的值。
#error text 强制编译器将 text 文字内容(直到第一个换行符为止)插入到着色器的信息日志当中
#pragma options 控制编译器的特定选项
#extension options 设置编译器支持特定 GLSL 扩展功能
#version number 设置当前使用的GLSL版本名称
#line options 设置诊断行号

2.5.2 宏定义

1
2
3
4
5
// 宏可以定义为单一的值
#define NUM_ELEMENTS 10

// 宏可以带有参数
#define LPos(n) gl_LightSource[(n)].position

此外, GLSL 还提供了一些预先定义好的宏,用于记录一些诊断信息(可以通过 #error 命令输出)

GLSL 预处理器中的预定义宏

宏名称 描述
_LINE_ 行号,默认为已经处理的所有换行符的个数加一,也可以通过 #line 命令修改
_FILE_ 当前处理的源字符串编号
_VERSION_ OpenGL着色语言版本的整数表示形式

可以通过 #undef 命令来取消之前定义过的宏( GLSL内置的宏除外):

1
#undef LPos

2.5.3 预处理器中的条件分支

1
2
3
#ifdef NUM_ELEMENTS
......
#endif // NUM_ELEMENTS
  • 可在 #if#elif 命令中使用操作符来进行判断
1
2
3
4
5
#if defined NUM_ELEMENTS && NUM_ELEMENTS > 3
......
#elif NUM_ELEMENTS < 7
......
#endif // defined NUM_ELEMENTS && NUM_ELEMENTS > 3

2.6 编译器的控制

2.6.1 编译器优化选项

1
2
3
4
// 启用优化选项
#pragma optimize(on)
// 禁用优化选项
#pragma optimize(off)

2.6.2 编译器调试选项

1
2
3
4
// 启用调试选项
#pragma debug(on)
// 禁用调试选项
#pragma debug(off)