WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)

544 阅读19分钟

前言

GLSL ES(Open Graphics Library Shading Language for Embedded Systems )是一门着色器语言,是在GLSL的基础上,删除和简化一部分功能之后形成的。用于在嵌入式和移动系统(包括游戏主机、手机、家电和车辆)上渲染高级 2D 和 3D 图形。它包含一个经过良好定义的桌面 OpenGL 子集,适合低功耗设备,并为软件和图形加速硬件之间提供了灵活且强大的接口。

本章将从以下几个方面介绍GLSL ES

  1. 数据、变量和变量类型
  2. 矢量、矩阵、结构体、数组、采样器(纹理)
  3. 运算、程序流、函数
  4. attribute、uniform和verying变量
  5. 精度限定词
  6. 预处理和指令

想要学习GLSL可以安装glsl-canvas插件,该扩展通过提供Show glslCanvas命令在VSCode中打开GLSL着色器的实时WebGL预览。

1 数据值类型(数值和布尔值)

****GLSL支持两种数据值类型

数值类型:GLSL ES支持整型数和浮点数,没有小数点的值被认为整型数,有小数点的被认为浮点数

布尔值类型:GLSL ES支持布尔值类型,包括true和false两个布尔常量

2 变量

****变量命名规范

  1. 只包括a-z、A-Z、0-9和下划线_
  2. 变量名的首字符不能是数字
  3. 不能是关键字,保留字
  4. 不能以gl_、webgl_或_webgl_开头,这些前缀以及被OpenGL ES保留了

以下是 GLSL ES 中的保留字和关键字的表格:

关键字

attributeboolbreakbvec2bvec3bvec4
constcontinuediscarddoelsefalse
floatforhightpifininout
intinvariantivec2ivec3ivec4lowp
mat2mat3mat4mediumoutprecision
returnsampler2Dsampler3Dstructtrueuniform
varyingvec2vec3vec4voidwhile

保留字

asmcastclassdefault
doubledvec2dvec3devc4
enumexternexternalfixed
flatfvec2fvec3fvec4
gotohalfhvec2hvec3
hvec4inlineinputinterface
longnamespacenoinlineoutput
packedpublicsampler1Dsampler1DShadow
sampler2DRectsampler2DRectShadowsampler2DShadowsampler3D
sampler3DRectshortsizeofstatic
superpswitchtemplatethis
typedefunionunsignedusing
volatile

GLSL ES是一门强类型语言,不像JavaScript,使用var、let或者const来声明变量,GLSL ES要求具体的指明变量类型。

3 矢量和矩阵

GLSL ES支持矢量和矩阵,这两种数据类型非常适合用来处理计算机图形,比如前面的片段着色器的颜色值计算,就用到了矩阵和矢量,以及高阶变换中也用到了矩阵计算顶点位置

矢量和矩阵类型

类别数据类型描述

矢量
vec2、vec3、vec4具有2、3、4个浮点数元素的矢量
ivec2、ivec3、ivec4具有2、3、4个整型数元素的矢量
bvec2、bvec3、bvec4具有2、3、4个布尔值元素的矢量
矩阵mat2、mat3、mat42阶、三阶、四阶浮点数矩阵

3.1 矢量构造函数

vec3 v3 = vec3(1.0, 2.0, 3.0)
vec2 v2 = vec2(v3) // 使用v3前两个元素(1.0,2.0)
vec4 v4 = vec4(1.0) // 将v4设为 (1.0,1.0,1.0,1.0)
vec4 v4b = vec4(v2,v4) // 矢量组合,使用v2,和v4矢量填充(1.0,2.0,1.0,1.0)
  

3.2 矩阵构造函数

向矩阵构造函数传入矩阵的 每一个元素的数值来构造矩阵,传入顺序必须是列主序(真实矩阵的转置格式)的。

mat4 m4 = mat4(
  1.0, 2.0, 3.0, 4.0,
  5.0, 6.0, 7.0, 8.0,
  9.0, 10.0, 11.0, 12.0,
  13.0, 14.0, 15.0, 16.0
)

向矩阵中传入一个或多个矢量

vec2 v2_1  = vec2(1.0,3.0)
vec2 v2_2 = vec2(2.0,4.0)
  // 1.0 2.0
  // 3.0 4.0
mat2 mat2_1 = mat2(v2_1,v2_2)

vec4 v4 = vec4(1.0,3.0,2.0,4.0)
  // 1.0 2.0
  // 3.0 4.0
mat2 mat2_2 = mat2(v4)

向矩阵构造函数中传入矢量和数值,剩余空间数值用矢量补齐

  // 1.0 2.0
  // 3.0 4.0
mat2 mat2_2 = mat2(1.0, 3.0 ,v2_2)

向矩阵构造函数传入单个数值

mat4 m4 = mat4( 1.0) // 会使用1.0自动填充整个矩阵

3.2.1 访问元素

为了访问矢量和矩阵中的元素,可以使用 . 或者[]运算符,下面将分节描述。

3.2.2 点运算符

在矢量变量名后接点运算符(.),然后接上分量名,就可以访问矢量元素了。

分量名

类别描述
x,y,z,w用来获取顶点坐标分量
r,g,b,a用来获取颜色分量
s,t,p,q获取纹理坐标分量

示例

   highp vec3  v3 = vec3(1.0,2.0,3.0);
   highp float f;
   f = v3.x; // 设f为1.0
   f = v3.y; // 设f为2.0
   f = v3.z; // 设f为3.0
   // x、r和s虽然名称不同,但都访问的是第一个分量
   f = v3.r; // 设f为1.0
   f = v3.s; // 设f为1.0
   // 超出矢量长度的分量访问会报错
   f = v3.w;

将多个分量名置于点运算符后,就可以从矢量中抽取出多个分量,这个过程称为混合

vec2 v2;
v2 = v3.xy; //设置 v2为(1.0,2.0)
v2 = v3.yz; //设置 v2为(2.0,3.0)
v2 = v3.xz; //设置 v2为(1.0,3.0)

3.2.3 []运算符

除了点运算符,还可以使用[]运算符通过数组下标来访问矢量或者矩阵的元素。注意,矩阵中的元素任然是按照列主序读取的。

示例

mat4 m4 = mat4(
  1.0, 2.0, 3.0, 4.0,
  5.0, 6.0, 7.0, 8.0,
  9.0, 10.0, 11.0, 12.0,
  13.0, 14.0, 15.0, 16.0
)

// 获取m4第一列元素 [1.0,2.0,3.0,4.0]
vec4 v4 = m4[0]
// 连续使用两个[]运算符可以获取某列第几个元素
vec4 v4 = m4[1][2] // 7.0
// []也可以结合矢量运算符使用
vec4 v4 = m4[2].y // 10.0

但是[]的使用有一个限制,[]中出现的索引值必须是一个常量,数值常量或者通过const声明的常量。

3.2.3 运算符

矩阵和矢量的运算符与基本类型运算符很类似,但是对于比较运算符只能用 == 和!=,

运算符运算适用数据类型
*乘法适用于vec[234]和mat[234]
/除法
+加法
-减法
++自增适用于vec[234]和mat[234]
--自减
=赋值适用于vec[234]和mat[234]
+=、-=、*=、/=运算赋值适用于vec[234]和mat[234]
==、!=比较运算符适用于vec[234]和mat[234],如果两个操作数的分量都相等赋予true,否则false

示例

矢量和浮点数运算

v3b = v3a + f
// b3b.x = v3a.x + f
// b3b.y = v3a.y + f
// b3b.z = v3a.z + f

矢量运算

v3c = v3a +v3b
// b3c.x = v3a.x + v3b.x
// b3c.y = v3a.y + v3b.y
// b3c.z = v3a.z + v3b.z

矩阵和浮点数运算

m3b = m3a * f
// m3b.x = m3a[0].x * f;
// m3b.y = m3a[0].y * f;
// m3b.z = m3a[0].z * f;

矩阵右乘矢量

v3b = m3a * v3a
// v3b.x = m3a[0].x * v3a.x +  m3a[1].x * v3a.y +  m3a[2].x * v3a.z
// v3b.y = m3a[0].y * v3a.x +  m3a[1].y * v3a.y +  m3a[2].y * v3a.z

矩阵右乘矢量结果还是一个矢量。

矩阵左乘矢量

v3b = v3a * m3a 
// v3b.x = m3a[0].x * v3a.x +  m3a[0].x * v3a.y +  m3a[0].x * v3a.z
// v3b.y = m3a[1].y * v3a.x +  m3a[1].y * v3a.y +  m3a[1].y * v3a.z

矩阵相乘

m3c = m3a * m3b 
// m3c[0].x = m3a[0].x * m3b[0].x +  m3a[1].x * m3b[0].y+  m3a[2].x * m3b[0].z

就是矩阵乘法

4 结构体

GLSL ES支持用户自定义的类型,**结构体,**使用关键字struct,将已存在的类型聚合在一起,就可以定义为结构体,例如

struct light{
  vec4 color
  vec3 position
}

light l1
light l2

上面这段代码声明了结构体light,它包含两个成员:color变量和position变量

此外为了方便还可以在定义结构体时,同时声明该结构体类型的变量:

struct light{
  vec4 color
  vec3 position
}l1

4.1 赋值和构造

构造体有标准的构造函数,其名称和结构体名一致。构造函数的参数的顺序必须与结构体中定义的成员顺序一致。

light l1 = light(vec4(0.0,1.0,2.0,3.0), vec3(0.0,1.0,2.0))

4.2 访问成员

在结构体变量后跟点运算符,然后加上成员名,就可以访问变量成员

vec4 = l1.color

4.3 运算符

结构体成员可以参与自身类型支持的任何运算,但是结构体本身只支持两种运算:赋值(=)和比较(==和!=),但是赋值和比较运算符不适用于含有数组与纹理成员的结构体。对于比较运算符,只有对于结构体内变量的所有成员都相等才为true。

4.4 数组

GLSL ES支持数组类型,与JavaScript数组类型不同,GLSL ES只支持一维数组,且数组对象不支持pop()、push()等操作,声明数组很简单,只需要在变量名后加上中括号([])和数组的长度。

float floatArray[4] // 声明含有4个浮点数的数组

数组长度必须是大于0的整型常量表达式,数组元素可以通过索引值来访问,和c语言一样,索引值从0开始,与JS和c不同的是,数组不能在声明时被一次性的初始化,而必须显示的对每个元素进行初始化

vec4 vec4Array[4]
vec4Array[0] = vec4(1.0,2.0,3.0,4.0)

数组本身只支持[]运算符,但是数组元素能够参与自身元素类型所支持的任意运算。

5 取样器(纹理)

GLSL ES支持一种名为取样器(sampler)的内置类型。我们必须通过该类型变量访问纹理。GLSL ES有两种基本的取样器类型:sampler2D和samplerCube。取样器变量只能是uniform变量,或者需要访问纹理的函数,如前面提到的texture2D函数参数,如:

uniform sampler2D u_Sampler;

此外唯一能赋值给取样器变量的就是纹理单元编号,且必须使用gl.uniformli()进行赋值,比如

// 将0号纹理传递给着色器
gl.uniform1i(u_Sampler, 0);

但是取样器类型变量受到着色器支持的纹理单元的最大数量限制。

5 continue、break和discard语句

continue和break跟其它语言定义一致,这里不做具体介绍。discard,它只能在片元着色器中起作用,表示放弃当前片元的处理,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲,discard应用

应用举例

一个场景就是我们需要一颗草(正方形图片)作为纹理,贴在一个2D四边形(Quad)上,然后将这个四边形放到场景中。如果不做处理的话,这颗草会完全贴在四边形上,且会覆盖四边形原有颜色(纹理是一个四边形,除了显示草的部分,其余部分都是透明的),当添加像草这样的植被到场景中时,不希望看到草的方形图像,而是只显示草的部分,并能看透图像其余的部分,也就是四边形自身颜色。因此就可以应用丢弃(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中,可以通过判断处理当前片元时,颜色值的透明度小于0.1就舍弃。

6 函数

与js不同,GLSL ES对于函数的定义更加接近C语言,

返回类型 函数名(参数){
  函数计算
  return 返回值
}

如果函数定义在其调用之后,必须事先声明:

float luma(vec4); // 规范声明
main(){
  ...
  float brightness = luma(color)
}
float luma (vec4 color){
  return ...
}

6.1 参数限定词

在GLSL ES中可以为函数参数指定参数限定词,以控制参数的行为,我们可以将函数定义成:

  1. 传递给函数的
  2. 将要在函数中被赋值的
  3. 即是传递给函数的,也是将要在函数中被赋值的
类别规则描述
in向函数传入值参数传入函数,在函数中可以使用参数的值,也可以修改其值。但函数内部的修改不会影响传入的变量
const in向函数传入值参数传入函数,在函数中可以使用参数的值,但不能被修改
out在函数中被赋值,并被传出传入变量的引用,若在函数内部被修改,会影响到函数外部传入的变量,但在函数调用之前,该参数的值是未定义的
inout传入函数,同时在函数中被赋值,并被传出传入变量的引用,若在函数内部被修改,会影响到函数外部传入的变量, 在函数调用之前,该参数可以有一个初始值
向函数传入值同in

举例:

out

void myFunction(out vec3 color) {
    color = vec3(1.0, 0.0, 0.0); // 赋值给 color
}

inout

void myFunction(inout vec3 color) {
    color += vec3(0.0, 1.0, 0.0); // 读取并修改 color
}

7 内置函数

以下是 GLSL ES 中的各种函数分类及其简要描述:

7.1 角度函数

  • radians(degrees): 将角度转换为弧度。
  • degrees(radians): 将弧度转换为角度。

7.2 三角函数

  • sin(x): 返回 x 的正弦值。
  • cos(x): 返回 x 的余弦值。
  • tan(x): 返回 x 的正切值。
  • asin(x): 返回 x 的反正弦值。
  • acos(x): 返回 x 的反余弦值。
  • atan(y, x): 返回 y/x 的反正切值。

7.3 指数函数

  • pow(x, y): 返回 xy 次幂。
  • exp(x): 返回 ex 次幂。
  • log(x): 返回 x 的自然对数。
  • exp2(x): 返回 2x 次幂。
  • log2(x): 返回 x 的以 2 为底的对数。

7.4 通用函数

  • abs(x): 返回 x 的绝对值。
  • min(x, y): 返回 xy 中的最小值。
  • max(x, y): 返回 xy 中的最大值。
  • clamp(x, minVal, maxVal): 将 x 限制在 minValmaxVal 之间。
  • mix(x, y, a): 返回 xy 的线性插值。

7.5 几何函数

  • length(v): 返回向量 v 的长度。
  • normalize(v): 返回单位向量,即将 v 归一化。
  • dot(x, y): 计算两个向量的点积。
  • cross(x, y): 计算两个三维向量的叉积。

7.6 矩阵函数

  • mat2(): 创建 2x2 矩阵。
  • mat3(): 创建 3x3 矩阵。
  • mat4(): 创建 4x4 矩阵。
  • transpose(m): 返回矩阵 m 的转置。
  • inverse(m): 返回矩阵 m 的逆矩阵。

7.7 矢量函数

  • vec2(x, y): 创建一个 2D 向量。
  • vec3(x, y, z): 创建一个 3D 向量。
  • vec4(x, y, z, w): 创建一个 4D 向量。
  • dot(v1, v2): 计算向量的点积。
  • cross(v1, v2): 计算向量的叉积(仅适用于 3D 向量)。

7.8 纹理查询函数

  • texture(sampler, coord): 从纹理中获取颜色值。
  • texture2D(sampler, coord): 适用于二维纹理的采样。
  • textureCube(sampler, coord): 从立方体纹理中获取颜色值。

这些函数为 GLSL ES 提供了丰富的数学运算和纹理处理能力,使得图形编程更加灵活和高效。

8 全局变量和局部变量

与js一样,GLSL ES也有全局变量和局部变量的概念,在函数外部声明的变量就是全局变量,在函数内部声明的变量就是局部变量。

9 存储限定词

9.1 const 变量

与js一样,通过const限定词声明的变量就不可更改,而且必须初始化。

9.2 attribute 变量

attribute变量只能出现在顶点着色器中,而且只能被声明为全局变量,被用来表示逐顶点的信息,这个逐顶点的意思是,比如线段有两个顶点(1.0,2.0,3.0)和(4.0,5.0,6.0),这两个坐标都会传递给attribute变量,而线段上的其它点比如中点,并没有传递给attribute变量,也未被顶点着色器处理过。attribute变量的类型只能是float、vec2、vec3、vec4、mat2、mat3、mat4。

顶点着色器中能够容纳的attribute变量的最大数目与设备有关,可以通过访问内置的全局常量来获取该值。在 GLSL 中,attributeuniformvarying 变量的数量限制因具体实现而异,通常取决于所用的 GPU 和驱动程序。以下是一些一般的指导原则:

在 GLSL ES 中,attributeuniformvarying 变量的数量限制通常为:

  1. Attribute 变量
  • 限制:通常在 8 到 16 之间。可以通过 gl.getIntegerv(gl.MAX_VERTEX_ATTRIBS, &value) 查询具体的限制。
  1. Uniform 变量
  • 限制:通常在 128 到 256 之间,具体数量可通过 gl.getIntegerv(gl.MAX_VERTEX_UNIFORM_VECTORS, &value) 查询。注意,这个值是以向量为单位的。
  1. Varying 变量
  • 限制:一般在 8 到 64 之间,具体数量可通过 gl.getIntegerv(gl.MAX_VARYING_VECTORS, &value) 查询。

查询限制的代码示例

可以在 WebGL 中使用以下代码查询这些限制:

const gl = canvas.getContext('webgl');

const maxAttributes = gl.getIntegerv(gl.MAX_VERTEX_ATTRIBS);
const maxUniformVectors = gl.getIntegerv(gl.MAX_VERTEX_UNIFORM_VECTORS);
const maxVaryingVectors = gl.getIntegerv(gl.MAX_VARYING_VECTORS);

console.log(`Max Attributes: ${maxAttributes}`);
console.log(`Max Uniform Vectors: ${maxUniformVectors}`);
console.log(`Max Varying Vectors: ${maxVaryingVectors}`);

这些限制可能会因硬件和驱动程序的不同而有所变化,因此在具体应用中最好进行动态查询。

总结

为了确保程序的兼容性,最好在代码中动态查询这些限制,而不是依赖于硬编码的值。这样可以确保你的着色器能够在不同的平台和设备上正常工作。

9.3 uniform变量

uniform变量可以用在顶点着色器和片元着色器中,且必须是全局变量,uniform变量是只读的,它可以是处理结构体和数组之外的任意类型。如果顶点着色器和片元着色器中声明了同名的uniform变量,那么它就会被这两种着色器共享。uniform包含了一致(非逐顶点/逐片元,各顶点或个片元共用)的数据,js应该向着色器传递该类数据。比如,变换矩阵就不是逐顶点的,而是所有顶点公用的,所以它在着色器中是uniform变量。

9.4 varying 变量

varying变量必须是全局变量,它的任务是从顶点着色器向片元着色器传输数据。我们可以在两种着色器中声明同名、同类型的varying类型变量。与attribute变量一样,varying变量的类型只能是float、vec2、vec3、vec4、mat2、mat3、mat4。需要注意的是,顶点着色器赋给片元着色器中的值,并不是直接传递给片元着色器中的varying变量,这其中发生了光栅化的过程:根据绘制的图形,对前者(顶点着色器varying变量)进行内插,然后再传递给后者(片元着色器varying变量)。正是应为varying变量需要内擦所以需要限制它的数据类型

10 精度限定词

GLSL ES 引入了精度限定词,目的是帮助着色器程序提高运行效率,消减内存开支。精度限定词用来表示每种数据具有的精度(比特数),高精度的程序需要更大的开销(包括内存和程序运行时间)。精度限定词是可选的。

默认值

#ifdef GL_ES
precision mediump float;
#endif

为什么引入精度限定词?WebGL应用程序是运行在不同硬件平台上。肯定存在某些情况下需要在低精度下运行程序,以提高内存使用效率,减少性能开销,以及更重要的降低能耗,延长移动设备的电池续航能力。但是在低精度下,WebGL程序运行结果比较粗糙或不准确,必须在程序效果和性能之间做出平衡。

精度限定词

精度限定词描述数值范围和精度(float,int)
highp高精度,顶点着色器的最低精度(-262,262)精度2-16(-216,216
mediump中精度,介于高精度和低精度之间,片元着色器的最低精度(-214,214)精度2-10(-210,210
lowp低精度,低于中精度,可以表示所有颜色(-2,2)精度2-8(-28,28

示例

mediump float size //中精度浮点型变量
highp vec4 position //高精度浮点型vec4对象

为每一种变量声明精度很麻烦,可以通过precision来声明着色器精度,这行代码必须在顶点着色器或者片元着色器顶部。

其格式如下:

precision 精度限定词 类型名称

这句代码表示,在着色器中,某种类型的变量其默认精度由精度限定词指定。

着色器会指定数据类型的默认精度

着色器类型数据类型默认精度
顶点着色器inthighp
floathighp
sampler2Dlowp
samplerCubelowp
片元着色器intmediump
float
sampler2Dlowp
samplerCubelowp

11 预处理指令

GLSL ES支持预处理指令。预处理指令用来在真正编译之前对代码进行预处理,都已#号开始。

比如:

#ifdef GL_ES
precision mediump float;
#endif

这段代码检查是否已经定义了GL_ES宏,如果是,那就执行#ifdef和#endif之间的代码:

GLSL ES中可能用到的三种预处理指令

#if 条件表达式
If 如果条件表达式为真,执行这里
#ednif

#ifdef 宏
如果定义了宏执行这里
#ednif

#ifndef 宏
如果没有定义宏执行这里
#ednif

12 宏

在 GLSL ES 中,宏的定义主要通过 #define 指令来实现。宏可以用来简化代码、提高可读性和重用性。下面是关于如何定义和使用宏的一些细节:

定义宏

可以使用 #define 语句来定义宏。语法如下:

#define 宏名 替换文本

示例

以下是一个简单的示例,展示如何在 GLSL ES 中定义和使用宏:

#define PI 3.14159265359
#define MAX(a, b) ((a) > (b) ? (a) : (b))

void main() {
    float angle = 45.0 * PI / 180.0; // 使用宏 PI
    float x = 5.0;
    float y = 10.0;
    float maxValue = MAX(x, y); // 使用宏 MAX
}

注意事项

  1. 宏展开: 在编译时,所有的宏都会被展开。这意味着,在代码中使用宏时,编译器会将其替换为定义时指定的文本。
  2. 参数宏: 在宏定义中可以使用参数(如 MAX(a, b)),这样可以创建更灵活的宏。
  3. 避免副作用: 使用参数宏时要小心,因为传递的参数会在宏展开时被多次计算,可能导致意想不到的副作用。

结束宏定义

可以使用 #undef 来取消定义一个宏。例如:

#undef PI

下面将进入三维世界