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

前言

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 中的保留字和关键字的表格:

关键字

attribute bool break bvec2 bvec3 bvec4
const continue discard do else false
float for hightp if in inout
int invariant ivec2 ivec3 ivec4 lowp
mat2 mat3 mat4 medium out precision
return sampler2D sampler3D struct true uniform
varying vec2 vec3 vec4 void while

保留字

asm cast class default
double dvec2 dvec3 devc4
enum extern external fixed
flat fvec2 fvec3 fvec4
goto half hvec2 hvec3
hvec4 inline input interface
long namespace noinline output
packed public sampler1D sampler1DShadow
sampler2DRect sampler2DRectShadow sampler2DShadow sampler3D
sampler3DRect short sizeof static
superp switch template this
typedef union unsigned using
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、mat4 2阶、三阶、四阶浮点数矩阵

3.1 矢量构造函数

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

3.2 矩阵构造函数

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

glsl 复制代码
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
)

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

glsl 复制代码
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)

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

glsl 复制代码
  // 1.0 2.0
  // 3.0 4.0
mat2 mat2_2 = mat2(1.0, 3.0 ,v2_2)

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

glsl 复制代码
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 获取纹理坐标分量

示例

glsl 复制代码
   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;

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

glsl 复制代码
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 []运算符

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

示例

glsl 复制代码
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

示例

矢量和浮点数运算

glsl 复制代码
v3b = v3a + f
// b3b.x = v3a.x + f
// b3b.y = v3a.y + f
// b3b.z = v3a.z + f

矢量运算

glsl 复制代码
v3c = v3a +v3b
// b3c.x = v3a.x + v3b.x
// b3c.y = v3a.y + v3b.y
// b3c.z = v3a.z + v3b.z

矩阵和浮点数运算

glsl 复制代码
m3b = m3a * f
// m3b.x = m3a[0].x * f;
// m3b.y = m3a[0].y * f;
// m3b.z = m3a[0].z * f;

矩阵右乘矢量

glsl 复制代码
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

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

矩阵左乘矢量

glsl 复制代码
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

矩阵相乘

glsl 复制代码
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,将已存在的类型聚合在一起,就可以定义为结构体,例如

glsl 复制代码
struct light{
  vec4 color
  vec3 position
}

light l1
light l2

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

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

glsl 复制代码
struct light{
  vec4 color
  vec3 position
}l1

4.1 赋值和构造

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

glsl 复制代码
light l1 = light(vec4(0.0,1.0,2.0,3.0), vec3(0.0,1.0,2.0))

4.2 访问成员

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

glsl 复制代码
vec4 = l1.color

4.3 运算符

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

4.4 数组

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

glsl 复制代码
float floatArray[4] // 声明含有4个浮点数的数组

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

glsl 复制代码
vec4 vec4Array[4]
vec4Array[0] = vec4(1.0,2.0,3.0,4.0)

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

5 取样器(纹理)

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

glsl 复制代码
uniform sampler2D u_Sampler;

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

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

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

5 continue、break和discard语句

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

应用举例

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

6 函数

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

glsl 复制代码
返回类型 函数名(参数){
  函数计算
  return 返回值
}

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

glsl 复制代码
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

glsl 复制代码
void myFunction(out vec3 color) {
    color = vec3(1.0, 0.0, 0.0); // 赋值给 color
}

inout

glsl 复制代码
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 中使用以下代码查询这些限制:

javascript 复制代码
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 引入了精度限定词,目的是帮助着色器程序提高运行效率,消减内存开支。精度限定词用来表示每种数据具有的精度(比特数),高精度的程序需要更大的开销(包括内存和程序运行时间)。精度限定词是可选的。

默认值

glsl 复制代码
#ifdef GL_ES
precision mediump float;
#endif

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

精度限定词

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

示例

glsl 复制代码
mediump float size //中精度浮点型变量
highp vec4 position //高精度浮点型vec4对象

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

其格式如下:

glsl 复制代码
precision 精度限定词 类型名称

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

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

着色器类型 数据类型 默认精度
顶点着色器 int highp
float highp
sampler2D lowp
samplerCube lowp
片元着色器 int mediump
float
sampler2D lowp
samplerCube lowp

11 预处理指令

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

比如:

glsl 复制代码
#ifdef GL_ES
precision mediump float;
#endif

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

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

glsl 复制代码
#if 条件表达式
If 如果条件表达式为真,执行这里
#ednif

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

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

12 宏

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

定义宏

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

glsl 复制代码
#define 宏名 替换文本

示例

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

glsl 复制代码
#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 来取消定义一个宏。例如:

glsl 复制代码
#undef PI

下面将进入三维世界

相关推荐
undefined&&懒洋洋4 分钟前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者2 小时前
React 19 新特性详解
前端
随云6322 小时前
WebGL编程指南之进入三维世界
前端·webgl
寻找09之夏3 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
多多米10054 小时前
初学Vue(2)
前端·javascript·vue.js
柏箱4 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑4 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8564 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
看到请催我学习4 小时前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript