GLSL 详解(基础篇)

GLSL 均指 OpenGL ES 2.0 的着色语言

OpenGL ES 的渲染管线包含有一个可编程的顶点阶段的一个可编程的片段阶段。其余的阶段则有固定的功能,应用程序对其行为的控制非常有限。每个可编程阶段中编译单元的集合组成了一个着色器。在OpenGL ES 2.0 中,每个着色器只支持一个编译单元。着色程序则是一整套编译好并链接在一起的着色器的集合。着色器 shader 的编写需要使用着色语言 GL Shader Language(GLSL),GLSL 的语法与 C 语言很类似。

如下是一个简单的着色器:

  1. // 顶点着色器 .vsh
  2. attribute vec4 position;
  3. attribute vec4 color;
  4. varying vec4 colorVarying;
  5. void main(void) {
  6. colorVarying = color;
  7. gl_Position = position;
  8. }
  9. // 片段着色器 .fsh
  10. varying lowp vec4 colorVarying;
  11. void main(void) {
  12. gl_FragColor = colorVarying;
  13. }

习惯上,我们一般把顶点着色器命名为 xx.vsh,片段着色器命名为 xx.fsh。
和 C 语言程序对应,用 GLSL 写出的着色器,它同样包括:

  • 变量 position
  • 变量类型 vec4
  • 限定符 attribute
  • main 函数
  • 基本赋值语句 colorVarying = color
  • 内置变量 gl_Position

学习一门语言,我们无非是从变量类型,结构体,数组,语句,函数,限定符等方面展开。下面,我们就照着这个顺序,学习 GLSL。

使用 GLSL 构建着色器

1. 变量

变量类别 变量类型 描述
void 用于无返回值的函数或空的参数列表
标量 float, int, bool 浮点型,整型,布尔型的标量数据类型
浮点型向量 float, vec2, vec3, vec4 包含1,2,3,4个元素的浮点型向量
整数型向量 int, ivec2, ivec3, ivec4 包含1,2,3,4个元素的整型向量
布尔型向量 bool, bvec2, bvec3, bvec4 包含1,2,3,4个元素的布尔型向量
矩阵 mat2, mat3, mat4 尺寸为2x2,3x3,4x4的浮点型矩阵
纹理句柄 sampler2D, samplerCube 表示2D,立方体纹理的句柄

除上述之外,着色器中还可以将它们构成数组或结构体,以实现更复杂的数据类型。

变量构造器和类型转换

对于变量运算,GLSL 中有非常严格的规则,即只有类型一致时,变量才能完成赋值或其它对应的操作。可以通过对应的构造器来实现类型转换。

标量

标量对应 C 语言的基础数据类型,它的构造和 C 语言一致,如下:

float myFloat = 1.0;
bool myBool = true;

myFloat = float(myBool);     // bool -> float
myBool = bool(myFloat);     // float -> bool

向量

当构造向量时,向量构造器中的各参数将会被转换成相同的类型(浮点型、整型或布尔型)。往向量构造器中传递参数有两种形式:

  • 如果向量构造器中只提供了一个标量参数,则向量中所有值都会设定为该标量值。
  • 如果提供了多个标量值或提供了向量参数,则会从左至右使用提供的参数来给向量赋值,如果使用多个标量来赋值,则需要确保标量的个数要多于向量构造器中的个数。

向量构造器用法如下:

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

vec3 temp = vec3(myVec3);             // temp = myVec3
vec2 myVec2 = vec2(myVec3);         // myVec2 = {myVec3.x, myVec3.y}

myVec4 = vec4(myVec2, temp, 0.0);   // myVec4 = {myVec2.x, myVec2.y , temp, 0.0 }

矩阵

矩阵的构造方法则更加灵活,有以下规则:

  • 如果对矩阵构造器只提供了一个标量参数,该值会作为矩阵的对角线上的值。例如 mat4(1.0) 可以构造一个 4 × 4 的单位矩阵
  • 矩阵可以通过多个向量作为参数来构造,例如一个 mat2 可以通过两个 vec2 来构造
  • 矩阵可以通过多个标量作为参数来构造,矩阵中每个值对应一个标量,按照从左到右的顺序

除此之外,矩阵的构造方法还可以更灵活,只要有足够的组件来初始化矩阵,其构造器参数可以是标量和向量的组合。在 OpenGL ES 中,矩阵的值会以的顺序来存储。在构造矩阵时,构造器参数会按照列的顺序来填充矩阵,如下:

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

向量和矩阵的分量

单独获得向量中的组件有两种方法:即使用 "." 符号或使用数组下标方法。依据构成向量的组件个数,向量的组件可以通过 {x, y, z, w}{r, g, b, a}{s, t, r, q} 等 swizzle 操作来获取。之所以采用这三种不同的命名方法,是因为向量常常会用来表示数学向量、颜色、纹理坐标等。其中的xrs 组件总是表示向量中的第一个元素,如下表:

分量访问符 符号描述
(x,y,z,w) 与位置相关的分量
(r,g,b,a) 与颜色相关的分量
(s,t,p,q) 与纹理坐标相关的分量

不同的命名约定是为了方便使用,所以哪怕是描述位置的向量,也是可以通过 {r, g, b, a} 来获取。但是在使用向量时不能混用不同的命名约定,即不能使用 .xgr 这样的方式,每次只能使用同一种命名约定。当使用 "." 操作符时,还可以对向量中的元素重新排序,如下:

vec3 myVec3 = vec3(0.0, 1.0, 2.0); // myVec3 = {0.0, 1.0, 2.0}
vec3 temp;
temp = myVec3.xyz; // temp = {0.0, 1.0, 2.0}
temp = myVec3.xxx; // temp = {0.0, 0.0, 0.0}
temp = myVec3.zyx; // temp = {2.0, 1.0, 0.0}

除了使用 "." 操作符之外,还可以使用数组下标操作。在使用数组下标操作时,元素 [0] 对应的是 x,元素 [1] 对应 y,以此类推。值得注意的是,在 OpenGL ES 2.0 中的某些情况下,数组下标不支持使用非常数的整型表达式(如使用整型变量索引),这是因为对于向量的动态索引操作,某些硬件设备处理起来很困难。在 OpenGL ES 2.0 中仅对 uniform 类型的变量支持这种动态索引。

矩阵可以认为是向量的组合。例如一个 mat2 可以认为是两个 vec2,一个 mat3 可以认为是三个 vec3 等等。对于矩阵来说,可以通过数组下标 “[]” 来获取某一列的值,然后获取到的向量又可以继续使用向量的操作方法,如下:

mat4 myMat4 = mat4(1.0);     // Initialize diagonal to 1.0 (identity)
vec4 col0 = myMat4[0];        // Get col0 vector out of the matrix 
float m1_1 = myMat4[1][1];  // Get element at [1][1] in matrix 
float m2_2 = myMat4[2].z;   // Get element at [2][2] in matrix

向量和矩阵的操作

绝大多数情况下,向量和矩阵的计算是逐分量进行的(component-wise)。当运算符作用于向量或矩阵时,该运算独立地作用于向量或矩阵的每个分量。
以下是一些示例:

vec3 v, u;
float f;
v = u + f;

等价于:

v.x = u.x + f;
v.y = u.y + f;
v.z = u.z + f;
vec3 v, u, w;
w = v + u;

等价于:

w.x = v.x + u.x;
w.y = v.y + u.y;
w.z = v.z + u.z;

2. 数组

除了结构体外,GLSL 中还支持数组。 语法与 C 语言相似,创建数组的方式如下代码所示:

float floatArray[4];
vec4 vecArray[2];

与C语言不同,在GLSL中,关于数组有两点需要注意:

  • 除了 uniform 变量之外,数组的索引只允许使用常数整型表达式。
  • 在 GLSL 中不能在创建的同时给数组初始化,即数组中的元素需要在定义数组之后逐个初始化,且数组不能使用 const 限定符。

    3. 函数

    GLSL 着色器同样是从 main 函数开始执行。另外, GLSL 也支持自定义函数。当然,如果一个函数在定以前被调用,则需要先声明其原型。

值得注意的一点是,GLSL 中函数不能够递归调用,且必须声明返回值类型(无返回值时声明为void)。如下:

vec4 getPosition(){ 
    vec4 v4 = vec4(0.,0.,0.,1.);
    return v4;
}

void doubleSize(inout float size){
    size= size*2.0  ;
}
void main() {
    float psize= 10.0;
    doubleSize(psize);
    gl_Position = getPosition();
    gl_PointSize = psize;
}

4. 限定符

存储限定符

在声明变量时,应根据需要使用存储限定符来修饰,类似 C 语言中的说明符。GLSL 中支持的存储限定符见下表:

限定符 描述
< none: default > 局部可读写变量,或者函数的参数
const 编译时常量,或只读的函数参数
attribute 由应用程序传输给顶点着色器的逐顶点的数据
uniform 在图元处理过程中其值保持不变,由应用程序传输给着色器
varying 由顶点着色器传输给片段着色器中的插值数据
  • 本地变量和函数参数只能使用 const 限定符,函数返回值和结构体成员不能使用限定符。
  • 数据不能从一个着色器程序传递给下一个阶段的着色器程序,这样会阻止同一个着色器程序在多个顶点或者片段中进行并行计算。
  • 不包含任何限定符或者包含 const 限定符的全局变量可以包含初始化器,这种情况下这些变量会在 main() 函数开始之后第一行代码之前被初始化,这些初始化值必须是常量表达式。
  • 没有任何限定符的全局变量如果没有在定义时初始化或者在程序中被初始化,则其值在进入 main() 函数之后是未定义的。
  • uniform、attribute 和 varying 限定符修饰的变量不能在初始化时被赋值,这些变量的值由 OpenGL ES 计算提供。
    默认限定符
    如果一个全局变量没有指定限定符,则该变量与应用程序或者其他正在运行的处理单元没有任何联系。不管是全局变量还是本地变量,它们总是在自己的处理单元被分配内存,因此可以对它们执行读和写操作。
    const 限定符
    任意基础类型的变量都可以声明为常量。常量表示这些变量中的值在着色器中不会发生变化,声明常量只需要在声明时加上限定符 const 即可,声明时必须赋初值。
    const float zero = 0.0;
    const float pi = 3.14159;
    const vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
    const mat4 identity = mat4(1.0);
    
    attribute 限定符
    GLSL 中另一种特殊的变量类型是 attribute 变量。
    attribute 变量只用于顶点着色器中,用来存储顶点着色器中每个顶点的输入(per-vertex inputs)。attribute 通常用来存储位置坐标、法向量、纹理坐标和颜色等。
    注意 attribute 是用来存储单个顶点的信息。如下是包含位置,色值 attribute 的顶点着色器示例: ```c // 顶点着色器 .vsh attribute vec4 position; attribute vec4 color;

varying vec4 colorVarying;

void main(void) { colorVarying = color; gl_Position = position; }

<a name="VHwaA"></a>
##### uniform 限定符
uniform 是 GLSL 中的一种变量类型限定符,用于存储应用程序通过 GLSL 传递给着色器的只读值。uniform 可以用来存储着色器需要的各种数据,如变换矩阵、光参数和颜色等。传递给着色器的在所有的顶点着色器和片段着色器中保持不变的的任何参数,基本上都应该通过 uniform 来存储。uniform 变量在全局区声明,以下是 uniform 的一些示例:
```c
uniform mat4 viewProjMatrix;
uniform mat4 viewMatrix;
uniform vec3 lightPosition;

需要注意的一点是,顶点着色器和片段着色器共享了 uniform 变量的命名空间。对于连接于同一个着色程序对象的顶点和片段着色器,它们共用同一组 uniform 变量,因此,如果在顶点着色器和片段着色器中都声明了 uniform 变量,二者的声明必须一致。当应用程序通过 API 加载了 uniform 变量时,该变量的值在顶点和片段着色器中都能够获取到。

varying 限定符

GLSL 中最后一个要说的存储限定符是 varying。varying 存储的是顶点着色器的输出,同时作为片段着色器的输入,通常顶点着色器都会把需要传递给片段着色器的数据存储在一个或多个 varying 变量中。这些变量在片段着色器中需要有相对应的声明且数据类型一致,然后在光栅化过程中进行插值计算。以下是一些 varying 变量的声明:

varying vec2 texCoord;
varying vec4 color;

顶点着色器和片段着色器中都会有 varying 变量的声明,由于 varying 是顶点着色器的输出且是片段着色器的输入,所以两处声明必须一致。与 uniform 和 attribute 相同,varying 也有数量的限制,可以使用 gl_MaxVaryingVectors 获取或使用 glGetIntegerv 查询 GL_MAX_VARYING_VECTORS 来获取。OpenGL ES 2.0 实现中的 varying 变量最小支持数为 8。

invariant 限定符

invariant 可以作用于顶点着色器输出的任何一个 varying 变量。

使用 invariant 限定符可以使输出的变量保持不变。invariant 限定符可以作用于之前已声明的变量使其具有不变性,也可以在声明变量时直接作为声明的一部分,可参考以下两段示例代码:

varying mediump vec3 Color;
// 使已存在的 color 变量不可变
invariant Color;

// 或
invariant varying mediump vec3 Color;

参数限定符

GLSL 提供了一种特殊的限定符用来定义某个变量的值是否可以被函数修改,详见下表:

限定符 描述
in 默认使用的缺省限定符,指明参数传递的是值,并且函数不会修改传入的值(C 语言中值传递)
inout 指明参数传入的是引用,如果在函数中对参数的值进行了修改,当函数结束后参数的值也会修改(C 语言中引用传递)
out 参数的值不会传入函数,但是在函数内部修改其值,函数结束后其值会被修改

精度限定符

OpenGL ES 与 OpenGL 之间的一个区别就是在 GLSL 中引入了精度限定符。
精度限定符可使着色器的编写者明确定义着色器变量计算时使用的精度,变量可以选择被声明为低、中或高精度。
精度限定符可告知编译器使其在计算时缩小变量潜在的精度变化范围,当使用低精度时,OpenGL ES 的实现可以更快速和低功耗地运行着色器,效率的提高来自于精度的舍弃,如果精度选择不合理,着色器运行的结果会很失真。

OpenGL ES 对各硬件并未强制要求多种精度的支持。其实现可以使用高精度完成所有的计算并且忽略掉精度限定符,然而某些情况下使用低精度的实现会更有优势,精度限定符可以指定整型或浮点型变量的精度,如 lowpmediump,及 highp,如下:

限定符 描述
highp 满足顶点着色语言的最低要求。对片段着色语言是可选项
mediump 满足片段着色语言的最低要求,其对于范围和精度的要求必须不低于lowp并且不高于highp
lowp 范围和精度可低于mediump,但仍可以表示所有颜色通道的所有颜色值

具体用法参考以下示例:

highp vec4 position;
varying lowp vec4 color;
mediump float specularExp;

除了精度限定符,还可以指定默认使用的精度。如果某个变量没有使用精度限定符指定使用何种精度,则会使用该变量类型的默认精度。默认精度限定符放在着色器代码起始位置,以下是一些用例:

precision highp float;
precision mediump int;

webgl笔记

WebGL 使得在支持HTML 的 canvas 标签的浏览器中,不需要安装任何插件,便可以使用基于 OpenGL ES 2.0 的 API 在 canvas 中进行2D和3D渲染。
WebGL程序包括用 JavaScript 写的控制代码,以及在图形处理单元(GPU, Graphics Processing Unit)中执行的着色代码(GLSL,注:GLSL为OpenGL着色语言)。WebGL 元素可以和其他 HTML 元素混合使用,并且可以和网页其他部分或者网页背景结合起来。

着色器

顶点着色器

每次渲染一个形状时,顶点着色器会在形状中的每个顶点运行。 它的工作是将输入顶点从原始坐标系转换到WebGL使用的缩放空间(clipspace)坐标系,其中每个轴的坐标范围从-1.0到1.0,并且不考虑纵横比,实际尺寸或任何其他因素。

顶点着色器需要对顶点坐标进行必要的转换,在每个顶点基础上进行其他调整或计算,然后通过将其保存在由GLSL提供的特殊变量(我们称为gl_Position)中来返回变换后的顶点