前言

在 OpenGL 开发中,不得不提到着色语言,着色语言用于编写顶点着色器和片段着色器。接下来就简单介绍一下着色语言的一些基础语法。

备注:内容来自于《OpenGL ES 3.0编程指南》一书。

一、版本规范

在 OpenGL ES 3.0 中,顶点着色器和片段着色器的第一行总是声明着色器版本。
使用如下语法声明着色器使用着色语言3.00版本:

#version es 300

没有声明或声明 #version es 100 则认定为使用着色语言1.00版本,即 OpenGL ES 2.0 中所使用的版本。

二、变量和变量类型

OpenGL ES 着色语言中的数据类型为:

变量分类

类型

描述

标量

float, int, uint, bool

浮点,整数,无符号整数,布尔值的标量类型

浮点向量

float, vec2, vec3, vec4

有 1、2、3、4 个分量,基于浮点的向量类型

整数向量

int, ivec2, ivec3, ivec4

有 1、2、3、4 个分量,基于整数的向量类型

无符号整数向量

uint, uvec2, uvec3, uvec4

有 1、2、3、4 个分量,基于无符号整数的向量类型

布尔向量

bool, bvec2, bvec3, bvec4

有 1、2、3、4 个分量,基于布尔的向量类型

矩阵

mat2x2(mat2), mat2x3, mat2x4, mat3x2, mat3x3(mat3), mat3x4, mat4x2, mat4x3, mat4x4(mat4)

各种大小的浮点矩阵,第一个数字表示列数,第二个数字表示行数

每个变量都必须有类型的声明,使用示例:

float angle; // 浮点标量
vec4 vPosition; // 有4个分量的浮点向量
mat4 mViewProjection; // 4*4大小的浮点矩阵
ivec2 vOffset; // 有2个分量的整数向量

三、变量构造器

着色语言中对于变量类型有非常严格的限制,只能在相同类型的变量间进行复制和运算,不允许隐含类型的转换。

为了进行类型转换,可以还有构造器初始化变量,每种内建变量类型都有一组相关的构造器。

1. 标量构造器

float myFloat = 1.0;
float myFloat2 = 1; // 错误:非法的类型转换
int myInt = 1;
float myFloat3 = float(myInt); // 正确:将int转为float

2. 向量构造器

遵循两个基本方法:

  • 如果只有一个标量参数,则该值用于设置向量的所有值。
  • 如果提供多个标量或者向量参数,则按从左向右的顺序赋值。

示例:

vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0}
vec3 myVec3 = vec3(0.0, 0.5, 1.0); // myVec3 = {0.0, 0.5, 1.0}
vec2 myVec2 = vec2(myVec3); // myVec2 = {0.0, 0.5}
myVec4 = vec4(myVec2, myVec3); // myVec4 = {0.0, 0.5, 0.0, 0.5}

3. 矩阵构造器

基本原则:

  • 如果只有一个标量参数,则该值用于矩阵对角线上。例如 mat4(1.0) 将创建一个4*4的单位矩阵。
  • 可以使用多个向量参数构造。例如 mat2 可以从两个 vec2 构造。
  • 可以还有多个标量参数构造,每个参数表示矩阵中的一个值,从左到右使用。

示例:

mat3 myMat3 = mat3(1.0, 0.0, 0.0, // 第一列 
                   0.0, 1.0, 0.0, // 第二列
                   0.0, 1.0, 1.0); // 第三列

四、向量和矩阵分量

1. 向量的分量

可以使用点运算符 . 或者数组下标 [] 两种方式访问向量的分量。

使用点运算符时,每个分量可以使用 向量坐标 {x, y, z, w}颜色坐标 {r, g, b, a} 或者 纹理坐标 {s, t, p, q} 三种命名组合访问。不同的命名约定只是为了方便,但是不能交叉混合使用不同的命名约定。

示例:

vec3 myVec3 = vec3(0.0, 1.0, 2.0);

vec3 temp = myVec3.xyz; // temp = {0.0, 1.0, 2.0}
temp = myVec3.xxx; // temp = {0.0, 0.0, 0.0}
temp = myVec3.bgr; // temp = {2.0, 1.0, 0.0}

使用数组下标时,元素 [0] 对应 x,元素 [1] 对应 y,等等。

2. 矩阵的分量

矩阵可以看作是由一些向量组成。对于矩阵,单独的列可以使用数组下标 [] 访问,这样就得到了矩阵中的一个向量了,然后每个向量就可以通过向量的访问方式来访问了。

示例:

mat4 myMat4 = mat4(1.0); // 4*4的单位矩阵

vec4 col0 = myMat4[0]; // 得到矩阵的第一列
float m1_1 = myMat4[1][1]; // 得到矩阵中[1][1]的元素
float m2_2 = myMat4[1].z; // 得到矩阵中[2][2]的元素

五、常量

通过在声明中加入 const 限定符,可以将变量声明为常量,常量只可读,不可修改。常量必须在声明时初始化。

示例:

const float pi = 3.14159;
const vec4 red = vec4(1.0, 0.0, 0.0, 0.0);
const mat4 identity = mat4(1.0);

六、结构体

类似于 C 语言中的结构体,着色语言也可以将一些变量聚合成结构。

示例:

struct fogStruct {
    vec4 color;
    float start;
    float end;
} fogVar;

fogVar = fogStruct(vec4(0.0, 1.0, 0.0, 0.0), // color
                   0.5, // start
                   2.0); // end
vec4 color = fogVar.color;
float start = fogVar.start;
float end = fogVar.end;

七、数组

着色语言也支持数组,同样类似于 C 语言中的数组。其中构造器中的参数数量必须等于数组的大小。

示例:

float a[4] = float[]{1.0, 2.0, 3.0, 4.0};
float b[4] = float[4]{1.0, 2.0, 3.0, 4.0};
vec2 c[2] = vec2[2](vec2(1.0), vec2(2.0));

八、运算符

OpenGL ES 着色语言的运算符:

运算符类型

描述

运算符类型

描述

*


==,!=,<,>,<=,>=

比较运算符

/


&&

逻辑与

%

取模

^^

逻辑异或

+


||

逻辑或

-


<<,>>

移位

++

递增

&,^,|

按位与、异或、或


递减

?:

三目运算符

=

赋值

,

序列

+=,-=,*=,/=

算术赋值

使用上和 C 语言中一样,值得注意的是,由于存在严格的类型规则,运算符只能出现在相同基本类型之间。

另外,除了 ==!= 之外,比较运算符(<<=>>=)只能用于标量。要比较向量,可以使用内建函数,逐个分量进行比较。

九、函数

函数的声明和 C 语言类似,且函数在定以前使用的话,必须先提供原型声明。着色语言提供特殊的限定符,定义函数是否可以修改可变参数。

限定符:

限定符

描述

in

该值为默认,表示参数按值传递,函数不能修改

out

表示该变量的值不传入函数,在函数返回时将被修改

inout

表示参数按引入传递,在函数返回时将被修改

示例:

// 计算基本漫射光线
vec4 diffuse(vec3 normal, vec3 light, vec4 baseColor) {
    return baseColor * dot(normal, light);
}

注意:在着色语言中,函数不能递归。

十、内建函数

OpenGL ES 着色语言有许多内建函数,处理各种计算任务。为了高效编写着色器,必须熟悉常见的内建函数。

示例:(片段着色器中计算基本反射照明的代码片段)

float nDotL = dot(normal, light);
float rDotV = dot(viewDir, (2.0 * normal) * nDotL - light);
float specular = specularColor * pow(rDotV, specularPower);

上例使用 dot 计算两个向量的点积,使用 pow 计算标量的幂次。内建函数数量诸多,限于篇幅这里不作详细介绍了,想要了解的可以查阅相关资料。

十一、控制流语句

着色语言中的控制流语句的语法和 C 语言也是类似的。不过条件语句中的测试表达式的值必须是一个布尔值。

if-then-else 使用示例:

if (color.a < 0.25) {
    color *= color.a;
} else {
    color = vec4(0.0);
}

当然也有 whiledo-while 循环。

在 OpenGL ES 2.0 中,循环的使用存在非常严格的管控规则,但那些限制在 3.0 中不复存在了。但这并不表示循环在性能上没有影响,经验法则是,应该尝试限制跨顶点/片段的扩散性流控或者循环迭代的使用。

十二、统一变量

统一变量在全局作用域使用 uniform 限定符声明。统一变量的命名空间在顶点着色器和片段着色器中都是共享的。

示例:

uniform mat4 viewProjMatrix;
uniform mat4 viewMatrix;
uniform vec3 lightPosition;

统一变量通常存储应用程序通过API传入着色器的只读值,如变换矩阵、照明参数和颜色等。

十三、统一变量块

统一变量块是 3.0 新增的一个东西,可使用于 统一变量缓冲区对象。利用统一变量缓冲区对象,统一缓冲区数据可以在多个程序中共享,只需要设置一次。并且在统一变量缓冲区对象之间切换比一次单独加载一个统一变量更高效。

使用示例:

#version es 300

uniform TransformBlock {
    mat4 matViewProj;
    mat3 marNormal;
    mat3 matTexGen;
};

layout(location = 0) in vec4 a_position;

void main() {
    gl_Position = matViewProj * a_position;
}

统一变量块的布局限定符

一些可选的布局限定符可用于指定支持统一变量块的统一缓冲区对象在内存中的布局方式。

1. 所有可用于统一变量块的布局限定符

限定符

描述

shared(默认)

指定多个着色器或者多个程序中统一变量块的内存布局相同。使用这个限定符,不同定义中的 row_major/column_major 值必须相等。覆盖 std140 和 packed

packed

指定编译器可以优化统一变量块的内存布局。使用这个限定符,必须查询偏移位置,且统一变量块无法在顶点/片段着色器和程序间共享。覆盖 std140 和 shared

std140

指定统一变量块的布局基于 OpenGL ES 3.0 规范中定义的一组标准规则。覆盖 shared 和 packed

row_major

矩阵在内存中以行优先顺序布局

column(默认)

矩阵在内存中以列优先顺序布局

2. 给所有统一变量块设置默认布局

在全局作用域内,添加如下声明即可:

layout(packed, row_major) uniform;
3. 给单个统一变量块设置布局
layout(std140) uniform TransformBlock {
    mat4 matViewProj;
    layout(row_major) mat3 matNormal;
    mat3 matTexGen;
}

十四、顶点和片段着色器的输入输出

使用 in 关键字表示输入,out 关键字表示输出。

1. 顶点着色器示例:

#version es 300

uniform mat4 u_matViewProj;
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec3 a_color;
out vec3 v_color;

void main() {
    gl_Position = u_matViewProj * a_position;
    v_color = a_color;
}

顶点着色器中,输入变量通常存储位置、法线、纹理坐标和颜色这样的数据,数据由应用程序加载。

上例中我们在输入变量前加了 layout 限定符,用于指定顶点属性的索引、这个是可选的,如果没有指定,将由链接程序自动分配。

顶点着色器中,输出变量将被传递到片段着色器中。在片段着色器中,我们使用与之匹配的输入变量声明,例如 in vec3 v_color

2. 片段着色器示例:

#version es 300
precision mediump float;

in vec3 v_color; // input form vertex shader
layout(location = 0) out vec4 o_fragColor;

void main() {
    o_fragColor = vec4(v_color, 1.0);
}

片段着色器中,输入变量是顶点着色器中的输出变量,需要保证类型和变量名一致。

片段着色器通常输出一个或多个颜色。通常我们只渲染到一个颜色缓冲区,这时布局限定符是可选的。当渲染到多个目标时(MRT),我们可以使用布局限定符指定每个输出前往的目标。

十五、插值限定符

上例中我们指定了顶点着色器的输出变量,即片段着色器的输入变量 v_color。我们可以给其添加插值限定符,指明其使用的插值方式。

插值限定符

描述

smooth

默认方式,平滑着色,顶点着色器的输出变量在图元中线性插值

flat

平面着色,将一个顶点视为驱动顶点(取决于图元类型),该顶点的值用于图元中所有片段

centroid

质心采样,使用多重采样渲染时,该限定符可用于强制插值发生在被渲染图元内部,否则图元边缘可能出现伪像

示例:

// 顶点着色器中
smooth out vec3 v_color;

// 片段着色器中
smooth in vec3 v_color;

十六、预处理器和指令

和 C 语言类似,不同点是宏不能定义为带有参数。

1. 条件测试宏

#define
#undef
#if
#ifdef
#ifndef
#else
#elif
#endif

2. 其余常见宏

__LINE__    // 当前行的行数
__FILE__    // 在 OpenGL ES 3.0 中始终为 0
__VERSION__ // 版本号,例如 300
GL_ES       // 在着色语言中始终为 1

#error      // 导致着色器编译出错,并在信息日志中放入对应消息
#pragma     // 为编译器指定特定于实现的指令
#extension  // 启用和设置扩展行为

十七、精度限定符

较低的精度效率更高,较高的精度效果更高。这种提升效率是以精度为代价的,不合适的精度限定符可能会导致伪像。

精度限定符种类:

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

注意:

在顶点着色器中,如果没有指定默认精度,则 int 和 float 默认精度都是 highp。

在片段着色器中,浮点值没有默认精度,必须由开发者声明。

十八、不变性

由于编译器可能进行指令的重新排序的优化,指令重排导致两个着色器之间的等价计算不能保证结果一致,而导致一些诸如“深度冲突”等问题。

使用 invariant 关键字给变量声明不变性,编译器会保证相同的输入和计算,在所有的脚本中结果一致,但是会导致性能下降。

示例:

#version es 300

uniform mat4 u_matViewProj;
layout(location = 0) in vec4 a_position;
invariant gl_Position;

void main() {
    gl_Position = u_matViewProj * a_position;
}

使用 #pargma 指定所有变量全都不变:

#pargma STDGL invariant(all)

不变性只在必要时才使用,它会限制编译器所做的优化,更不建议指定所有变量全都不变。