WebGL Programing Guide 学习笔记

一些在 2D 中使用 shader 的基础知识,暂不涉及 3D。

GLSL ES 的一些有趣语法

参数修饰符

out 可以代替 return,此时返回值类型得改成 void。而 in 可以省略,这是参数的默认属性。

void luma2 (in vec3 color, out float brightness) {
    brightness = 0.2126 * color.r + 0.7162 * color.g + 0.0722 * color.b;
}

想起函数式编程中纯函数的概念,在 JS 中只能依靠约定保证参数在函数执行过程中不被修改。

精度修饰符

更高的精度显然需要更大的存储空间。GLSL 给予开发者权衡性能的选择。

#ifdef GL_ES
precision mediump float;
#endif

高效存储 vertex 数据

如果想绘制多个点,多次调用 drawArray() ,每次绘制一个点显然不是一个高效的方法:

// g_points = [x1, y1, x2, y2...]
for(var i = 0; i < len; i += 2) {
    // a_Position 为 attribute 地址,向地址中写入 xyz
    gl.vertexAttrib3f(a_Position, g_points[i], g_points[i+1], 0.0);
    // 从第 0 个 vertex 开始,绘制 1 个
    gl.drawArrays(gl.POINTS, 0, 1);
}

一次性向 vertex shader 传递多个顶点信息,就需要使用 Buffer 了:

  1. 创建一个 buffer 对象 gl.createBuffer()
  2. 绑定 buffer 对象到一个 target gl.bindBuffer()
  3. 向 buffer 对象写入数据 gl.bufferData()
  4. 分配 buffer 对象到 vertex shader 中的 attribute 变量 gl.vertexAttribPointer()
  5. 开启分配完成传值 gl.enableVertexAttribArray()

使用 Buffer 步骤

使用 Buffer 步骤

之前只是在 Buffer 中存储了各个顶点的位置信息,如果每个顶点有不同的大小呢? 使用多个 Buffer 看似可行,但是一旦顶点数量增多,或者每个顶点又需要不同颜色,多个 Buffer 的问题就显现出来了。 这时候就需要在同一个 Buffer 中 分组 存储全部的顶点信息,也叫 interleaving

var verticesSizes = new Float32Array([
    // 混合存储顶点坐标和大小
    0.0, 0.5, 10.0, // The 1st point
    -0.5, -0.5, 20.0, // The 2nd point
    0.5, -0.5, 30.0 // The 3rd point
]);

gl.vertexAttribPointer() 最后两个参数可以指定 stride 的长度以及当前顶点信息在每个 stride 中的偏移量(offset):

Stride & Offset

Stride & Offset

这样就实现了在一个 Buffer 中存储全部顶点信息。值得注意的是,目前我们存储的顶点信息都是给 vertex shader 使用的,如果想要给 fragment shader 传递变量,例如顶点颜色,该如何使用呢?

shader 间传递变量

从 vertex shader 向 fragment shader 传递变量需要使用 varying,为啥不是熟悉的 attribute 而需要这个额外的看起来像桥梁一样的新东西呢?

varying 变量

varying 变量

要弄清这个问题,我们得知道 shader 间传值并不是这么简单的,中间会经历两个步骤:

vertex shader 和 fragment shader 之间

vertex shader 和 fragment shader 之间

可见在 vertex shader 和 fragment shader 中虽然变量同名(v_Color),但事实上并不是一一对应的关系。这也是 varying 名称的由来,线性插值会生成 vertex 间像素的值:

线性插值

线性插值

在下面的例子中,三个顶点红绿蓝,借助线性插值,能生成平滑的效果:

See the Pen dmgLEK by xiaop (@xiaoiver) on CodePen.

书中的例子使用了 Google 编写的 webgl-utils,但是在各个 CDN 上都没有找到。我使用了 webglfundamentals.org 提供的 WebGL 工具库,完成加载 shader,创建 program 的工作,一定不能忘记调用 gl.useProgram()

texture 纹理

使用纹理是一个比较复杂的过程。

坐标转换

首先图片(PNG,JPEG)坐标系不同于 WebGL 坐标系,需要进行转换:

图片坐标到 WebGL 坐标映射

图片坐标到 WebGL 坐标映射

只需要反转一下 Y 轴即可:

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

然后我们需要将 texture 坐标映射到 vertex shader 坐标系,例如这里只是简单平移一下,并不存在缩放。最终显示在 -1 ~ 1 的坐标系中:

纹理坐标映射

纹理坐标映射

纹理坐标存储在 a_TexCoord 中:

var verticesTexCoords = new Float32Array([
    // Vertex coordinates, texture coordinate
    -0.5,  0.5,   0.0, 1.0,
    -0.5, -0.5,   0.0, 0.0,
     0.5,  0.5,   1.0, 1.0,
     0.5, -0.5,   1.0, 0.0,
]);
var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
// Assign the buffer object to a_TexCoord variable
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord);

创建纹理对象

WebGL 中规定浏览器实现中至少有 gl.TEXTURE0gl.TEXTURE7 这8个纹理单位(可能更多),每一个都可以关联到一种纹理类型,比如 gl.TEXTURE_2D。有点类似之前每一个 Buffer 对象都可以关联 gl.ARRAY_BUFFER 或者 gl.ARRAY_ELEMENT_BUFFER。甚至连创建语法也很相似:

// 创建 texture
var texture = gl.createTexture();
// Enable texture unit0
gl.activeTexture(gl.TEXTURE0);
// Bind the texture object to the target
gl.bindTexture(gl.TEXTURE_2D, texture);

关联纹理对象

关联纹理对象

指定纹理参数

完成了纹理对象的创建,激活和绑定,随后需要设置一些纹理参数。这些参数指定了在某些场景下纹理的规则:

4种纹理参数和效果

4种纹理参数和效果

这里不得不吐槽下 WebGL 内置的一些函数命名,虽然有些通过函数名就能判断参数类型和数目,但是像 texParameteri()vertexAttribPointer() 这样部分缩写实在很难记忆。

上传图片到纹理对象中

接下来需要将图片上传到纹理对象中。 在 WebGL 中,使用 new Image() 异步加载图片十分简单,以下操作都在成功回调函数中完成。 这里需要根据图片格式指定纹理数据格式,例如对于 PNG 格式的图片,纹理数据必须使用 gl.RGBA,而对于 JPEG 格式,则使用 gl.RGB

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

fragment shader 中的操作

在 fragment shader 中读取纹理对象需要通过 sampler 完成。这是一个常量 uniform sampler2D u_Sampler;

之前纹理对象存储在 gl.TEXTURE0 中,这里只需要传一个序号就行了。

gl.uniform1i(u_Sampler, 0);

根据从 vertex shader 中传过来的坐标,使用 sampler,fragment shader 就能获取当前纹理中每个 fragment 对应的颜色:

gl_FragColor = texture2D(u_Sampler, v_TexCoord);

总结下,完整使用 texture 流程如下:

纹理完整流程

纹理完整流程

多个纹理混合

首先异步加载多张图片,需要保证全部加载完成再进行后续操作。 在 fragment shader 中,由于颜色都是用 vec 向量表示,可以相乘,效果就是 rgba 各个部分相乘:

gl_FragColor = color0 * color1;

这种混合方式和 CSS 中 background-blend-mode一种混合方式一致。在 PS 中也有类似图层混合方法:

.simple-blended {
    background-image: url(image.jpg);
    background-color: red;
    background-blend-mode: multiply;
}