WebGL 理论基础 - 着色器和 GLSL

本文来源:https://zhuanlan.zhihu.com/p/50309444

----------------------------------------------------

此文上接 WebGL 基础概念。如果你还没有阅读 WebGL 工作原理,也许可以先 阅读这篇文章

我们之前提到过着色器和 GLSL,但是没有涉及细节,你可能已经对此有所了解,但以防万一,这里将详细讲解着色器和 GLSL。

在 工作原理 中我们提到,WebGL 每次绘制需要两个着色器,一个顶点着色器和一个片段着色器,每一个着色器都是一个方法。一个顶点着色器和一个片段着色器链接在一起放入一个着色程序中(或者只叫程序)。一个典型的 WebGL 应用会有多个着色程序。

顶点着色器

一个顶点着色器的工作是生成裁剪空间坐标值,通常是以下的形式

1void main() {
2   gl_Position = doMathToMakeClipspaceCoordinates
3}

每个顶点调用一次(顶点)着色器,每次调用都需要设置一个特殊的全局变量 gl_Position,该变量的值就是裁减空间坐标值。

顶点着色器需要的数据,可以通过以下三种方式获得。

  1. Attributes 属性 (从缓冲中获取的数据)

  2. Uniforms 全局变量 (在一次绘制中对所有顶点保持一致值)

  3. Textures 纹理 (从像素或纹理元素中获取的数据)

Attributes 属性

最常用的方法是缓冲和属性,在工作原理中讲到了缓冲和属性,你可以创建缓冲,

1var buf = gl.createBuffer();

将数据存入缓冲

1gl.bindBuffer(gl.ARRAY_BUFFER, buf);
2gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW);

然后初始化的时候,在你制作的(着色)程序中找到属性所在地址

1var positionLoc = gl.getAttribLocation(someShaderProgram, "a_position");

在渲染的时候告诉 WebGL 怎么从缓冲中获取数据传递给属性

 1// 开启从缓冲中获取数据
 2gl.enableVertexAttribArray(positionLoc);
 3
 4var numComponents = 3;  // (x, y, z)
 5var type = gl.FLOAT;    // 32位浮点数据
 6var normalize = false;  // 不标准化
 7var offset = 0;         // 从缓冲起始位置开始获取
 8var stride = 0;         // 到下一个数据跳多少位内存
 9                        // 0 = 使用当前的单位个数和单位长度 ( 3 * Float32Array.BYTES_PER_ELEMENT )
10
11gl.vertexAttribPointer(positionLoc, numComponents, type, false, stride, offset);

在 WebGL 基础概念 中示范了不做任何运算直接将数据传递给 gl_Position

1attribute vec4 a_position;
2
3void main() {
4   gl_Position = a_position;
5}

如果缓冲中存的是裁剪空间坐标就没什么问题。

属性可以用 floatvec2vec3vec4mat2mat3 和 mat4 数据类型。

Uniforms 全局变量

全局变量在一次绘制过程中传递给着色器的值都一样,在下面的一个简单的例子中,用全局变量给顶点着色器添加了一个偏移量

1attribute vec4 a_position;
2uniform vec4 u_offset;
3
4void main() {
5   gl_Position = a_position + u_offset;
6}

现在可以把所有顶点偏移一个固定值,首先在初始化时找到全局变量的地址

1var offsetLoc = gl.getUniformLocation(someProgram, "u_offset");

然后在绘制前设置全局变量

1gl.uniform4fv(offsetLoc, [1, 0, 0, 0]);  // 向右偏移一半屏幕宽度

要注意的是全局变量属于单个着色程序,如果多个着色程序有同名全局变量,需要找到每个全局变量并设置自己的值。我们调用gl.uniform???的时候只是设置了当前程序的全局变量,当前程序是传递给 gl.useProgram的最后一个程序。

全局变量有很多类型,对应的类型有对应的设置方法。

 1gl.uniform1f (floatUniformLoc, v);                 // float
 2gl.uniform1fv(floatUniformLoc, [v]);               // float 或 float array
 3gl.uniform2f (vec2UniformLoc,  v0, v1);            // vec2
 4gl.uniform2fv(vec2UniformLoc,  [v0, v1]);          // vec2 或 vec2 array
 5gl.uniform3f (vec3UniformLoc,  v0, v1, v2);        // vec3
 6gl.uniform3fv(vec3UniformLoc,  [v0, v1, v2]);      // vec3 或 vec3 array
 7gl.uniform4f (vec4UniformLoc,  v0, v1, v2, v4);    // vec4
 8gl.uniform4fv(vec4UniformLoc,  [v0, v1, v2, v4]);  // vec4 或 vec4 array
 9
10gl.uniformMatrix2fv(mat2UniformLoc, false, [  4x element array ])  // mat2 或 mat2 array
11gl.uniformMatrix3fv(mat3UniformLoc, false, [  9x element array ])  // mat3 或 mat3 array
12gl.uniformMatrix4fv(mat4UniformLoc, false, [ 16x element array ])  // mat4 或 mat4 array
13
14gl.uniform1i (intUniformLoc,   v);                 // int
15gl.uniform1iv(intUniformLoc, [v]);                 // int 或 int array
16gl.uniform2i (ivec2UniformLoc, v0, v1);            // ivec2
17gl.uniform2iv(ivec2UniformLoc, [v0, v1]);          // ivec2 或 ivec2 array
18gl.uniform3i (ivec3UniformLoc, v0, v1, v2);        // ivec3
19gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]);      // ivec3 or ivec3 array
20gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4);    // ivec4
21gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]);  // ivec4 或 ivec4 array
22
23gl.uniform1i (sampler2DUniformLoc,   v);           // sampler2D (textures)
24gl.uniform1iv(sampler2DUniformLoc, [v]);           // sampler2D 或 sampler2D array
25
26gl.uniform1i (samplerCubeUniformLoc,   v);         // samplerCube (textures)
27gl.uniform1iv(samplerCubeUniformLoc, [v]);         // samplerCube 或 samplerCube array

还有一些类型 boolbvec2bvec3, 和 bvec4,它们可用 gl.uniform?f? 或 gl.uniform?i?

一个数组可以一次设置所有的全局变量,例如

1// 着色器里
2uniform vec2 u_someVec2[3];
3
4// JavaScript 初始化时
5var someVec2Loc = gl.getUniformLocation(someProgram, "u_someVec2");
6
7// 渲染的时候
8gl.uniform2fv(someVec2Loc, [1, 2, 3, 4, 5, 6]);  // 设置数组 u_someVec2

如果你想单独设置数组中的某个值,就要单独找到该值的地址。

1// JavaScript 初始化时
2var someVec2Element0Loc = gl.getUniformLocation(someProgram, "u_someVec2[0]");
3var someVec2Element1Loc = gl.getUniformLocation(someProgram, "u_someVec2[1]");
4var someVec2Element2Loc = gl.getUniformLocation(someProgram, "u_someVec2[2]");
5
6// 渲染的时候
7gl.uniform2fv(someVec2Element0Loc, [1, 2]);  // set element 0
8gl.uniform2fv(someVec2Element1Loc, [3, 4]);  // set element 1
9gl.uniform2fv(someVec2Element2Loc, [5, 6]);  // set element 2

同样的,如果你创建了一个结构体

1struct SomeStruct {
2  bool active;
3  vec2 someVec2;
4};
5uniform SomeStruct u_someThing;

你需要找到每个元素的地址

1var someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active");
2var someThingSomeVec2Loc = gl.getUniformLocation(someProgram, "u_someThing.someVec2");

纹理(顶点着色器中)

同 Textures 纹理(在片段着色器中)。

片段着色器

一个片段着色器的工作是为当前光栅化的像素提供颜色值,通常是以下的形式

1precision mediump float;
2
3void main() {
4   gl_FragColor = doMathToMakeAColor;
5}

每个像素都将调用一次片段着色器,每次调用需要从你设置的特殊全局变量 gl_FragColor 中获取颜色信息。

片段着色器所需的数据,可以通过以下三种方式获取

  1. Uniforms 全局变量 (在一次绘制中对所有像素保持一致)

  2. Textures 纹理 (从纹理,数据像素中获取的数据)

  3. Varyings 可变量 (顶点着色器中提供并且经过差值后的数据)

Uniform 全局变量(片段着色器中)

同 Uniforms 全局变量。

Textures 纹理(片段着色器中)

在着色器中获取纹理信息,可以先创建一个 sampler2D 类型全局变量,然后用GLSL方法 texture2D 从纹理中提取信息。

1precision mediump float;
2
3uniform sampler2D u_texture;
4
5void main() {
6   vec2 texcoord = vec2(0.5, 0.5)  // 获取纹理中心的值
7   gl_FragColor = texture2D(u_texture, texcoord);
8}

从纹理中获取的数据取决于很多设置,至少要创建并给纹理填充数据,例如

 1var tex = gl.createTexture();
 2gl.bindTexture(gl.TEXTURE_2D, tex);
 3var level = 0;
 4var width = 2;
 5var height = 1;
 6var data = new Uint8Array([
 7   255, 0, 0, 255,   // 一个红色的像素
 8   0, 255, 0, 255,   // 一个绿色的像素
 9]);
10gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);

在初始化时找到全局变量的地址

1var someSamplerLoc = gl.getUniformLocation(someProgram, "u_texture");

在渲染的时候 WebGL 要求纹理必须绑定到一个纹理单元上

1var unit = 5;  // 挑选一个纹理单元
2gl.activeTexture(gl.TEXTURE0 + unit);
3gl.bindTexture(gl.TEXTURE_2D, tex);

然后告诉着色器你要使用的纹理在哪个纹理单元

1gl.uniform1i(someSamplerLoc, unit);

Varyings 可变量

工作原理提到过,可变量是一种顶点着色器给片段着色器传值的方式。

为了使用可变量,要在两个着色器中定义同名的可变量。给顶点着色器中可变量设置的值,会作为参考值进行差值,在绘制像素时传给片段着色器对应的可变量。

顶点着色器

 1attribute vec4 a_position;
 2
 3uniform vec4 u_offset;
 4
 5varying vec4 v_positionWithOffset;
 6
 7void main() {
 8  gl_Position = a_position + u_offset;
 9  v_positionWithOffset = a_position + u_offset;
10}

片段着色器

1precision mediump float;
2
3varying vec4 v_positionWithOffset;
4
5void main() {
6  // 从裁剪空间 (-1 <-> +1) 转换到颜色空间 (0 -> 1).
7  vec4 color = v_positionWithOffset * 0.5 + 0.5
8  gl_FragColor = color;
9}

上方的示例没有太多意义,通常情况下直接将裁剪空间的值传给片段着色器当作颜色值是没有意义的,虽然它可以运行并且可以生成颜色值。

GLSL

GLSL 全称是 Graphics Library Shader Language (图形库着色器语言),是着色器使用的语言。它有一些不同于 JavaScript的特性,主要目的是为栅格化图形提供常用的计算功能。所以它内建的数据类型例如vec2vec3 和 vec4 分别代表两个值,三个值和四个值,类似的还有mat2mat3 和 mat4 分别代表 2x2, 3x3 和 4x4 矩阵。

你可以做一些运算例如常量和矢量的乘法。

1vec4 a = vec4(1, 2, 3, 4);
2vec4 b = a * 2.0;
3// b 现在是 vec4(2, 4, 6, 8);

它同样可以做矩阵乘法以及矢量和矩阵的乘法

1mat4 a = ???
2mat4 b = ???
3mat4 c = a * b;
4
5vec4 v = ???
6vec4 y = c * v;

他还为矢量数据提供多种分量选择器,例如 vec4

1vec4 v;
  • v.x 和 v.s 以及 v.r , v[0] 表达的是同一个分量。

  • v.y 和 v.t 以及 v.g , v[1] 表达的是同一个分量。

  • v.z 和 v.p 以及 v.b , v[2] 表达的是同一个分量。

  • v.w 和 v.q 以及 v.a , v[3] 表达的是同一个分量。

它还支持矢量调制,意味者你可以交换或重复分量。

1v.yyyy


1vec4(v.y, v.y, v.y, v.y)

是一样的。

同样的

1v.bgra

1vec4(v.b, v.g, v.r, v.a)

等价。

当构造一个矢量或矩阵时可以一次提供多个分量,例如

1vec4(v.rgb, 1)


1vec4(v.r, v.g, v.b, 1)

是一样的。

同样

1vec4(1)

1vec4(1, 1, 1, 1)

相同。

值得注意的是 GLSL 是一个强类型的语言。

1float f = 1;  // 错误,1是int类型,不能将int型赋值给float

正确的方式是

1float f = 1.0;      // 使用float
2float f = float(1)  // 转换integer为float

上例中 vec4(v.rgb, 1) 不会因为 1 报错,因为 vec4 内部进行了转换类似 float(1) 。

GLSL 有一系列内置方法,其中大多数运算支持多种数据类型,并且一次可以运算多个分量,例如

1T sin(T angle)

T可以是 floatvec2vec3 或 vec4 。如果你传的是 vec4 返回的也是 vec4, 返回结果对应每个分量的正弦值。换句话说如果 v 是 vec4 类型。那么

1vec4 s = sin(v);


1vec4 s = vec4(sin(v.x), sin(v.y), sin(v.z), sin(v.w));

是一样的。
有时一个参数是浮点型而剩下的都是 T ,意思是那个浮点数据会作为所有其他参数的一个新分量。例如如果 v1 和 v2 是 vec4 同时 f 是浮点型,那么

1vec4 m = mix(v1, v2, f);


1vec4 m = vec4(
2  mix(v1.x, v2.x, f),
3  mix(v1.y, v2.y, f),
4  mix(v1.z, v2.z, f),
5  mix(v1.w, v2.w, f));

等价

你可以在 WebGL 引用表(https://www.khronos.org/files/webgl/webgl-reference-card-1_0.pdf)最后一页看到所有GLSL方法的列表。如果你喜欢干货以更详细的东西你可以看看 GLSL 规范(https://www.khronos.org/files/opengles_shading_language.pdf)。

总结

这是当前系列文章的重点。WebGL 的全部内容就是创建不同的着色器,向着色器提供数据然后调用 gl.drawArrays 或 gl.drawElements 让 WebGL 调用当前顶点着色器处理每个顶点,调用当前片段着色器渲染每个像素。

实际上创建着色器需要为数不多的几行代码,并且在大多数 WebGL 应用程序中都相似,因此一旦写完几乎可以不再关心它们了,以后将介绍如何编译 GLSL 并链接到着色程序。

END

获得更多信息

关注公众号

WebGL 理论基础 - 基础概念

WebGL 理论基础 - 工作原理

玄说前端面试层层提问—关于 redux 的面试题

玄说前端面试层层解析—关于 redux 的源码

技术交流qq群:850038125

微信群:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值