2023-03-29
从白噪声开始学习 WebGL
本文介绍白噪声生成, Audio 和 Pixel:
播放

# 声音怎么实现

利用 WebAudio 播放即可,不是本文重点

# Canvas 绘制思路

画白噪声单个像素点很容易, 利用 Math.random() 生成随机数即可。
然后将这个函数应用到 canvas 画布全部点上即可绘制出来一帧,最后让其动起来即可,具体实现如下:
00import React from 'react';
01
02export class WhiteNoiseCanvas {
03  private canvas: HTMLCanvasElement | null = null;
04  private g: CanvasRenderingContext2D | null = null;
05  private stop = true;
06  private inited = false;
07
08  public init = (canvas: HTMLCanvasElement) => {
09    this.canvas = canvas;
10    this.g = canvas.getContext('2d')!;
11    this.inited = true;
12
13    // 先绘制一帧, 绘制后暂停
14    this.stop = false;
15    this.render();
16    this.stop = true;
17
18    const animate = () => {
19      // deinit 后结束动画
20      if (!this.inited) return;
21
22      this.render();
23      requestAnimationFrame(animate);
24    }
25
26    animate();
27  }
28
29  public play = () => {
30    this.stop = !this.stop;
31  }
32
33  public deinit = () => {
34    this.inited = false;
35    this.g = null;
36    this.canvas = null;
37  }
38
39  private render = () => {
40    const { g, canvas, stop } = this;
41    if (stop) return; // 说明暂停
42    if (
43      !this.inited
44      || !g
45      || !canvas
46    ) return console.warn('no init');
47
48    // 建一张空白图像
49    const image = g.createImageData(
50      canvas.width,
51      canvas.height
52    );
53
54    // 遍历图像的每一个像素
55    const pixcels = canvas.width * canvas.height;
56    for (let i = 0; i < pixcels; i += 1) {
57      const noise = Math.random() * 255;
58      const d = noise > 127 ? 255 : 0;
59      image.data[i * 4] = d;
60      image.data[i * 4 + 1] = d;
61      image.data[i * 4 + 2] = d;
62      image.data[i * 4 + 3] = 255;
63    }
64
65    // 吧图像绘制到 canvas 上
66    g.putImageData(image, 0, 0);
67  }
68}
最后在合适的地方 new WhiteNoiseCanvas() 并 init() 即可
播放 (移动端慎点, 卡)

# WebGL 还是 Canvas

上段内容大致描述了绘制过程, 很明显上述过程涉及到遍历全部点的过程, 典型的计算密集型应用。这种场景小尺寸图片画布还好,如果需要绘制 4K 画布的时候就卡的不行了,我们需要一个更高性能的方案 —— webgl, 借助显卡加速来绘制

# WebGL 渲染管线

大致说来说,渲染管线是渲染像素的整个流程,如图:
loading...

顶点着色器 (Vertex shader)

通过编写顶点着色器可以让 GPU 帮你做顶点的坐标变换,在 3D 里面可以据此实现摄像机 (Camara),实现镜头跟踪等特性。
loading...
在 2D 场景下,这里变换主要是放大缩小等常用变换

图元装配 (Primitive assembly)

图元装配是将顶点着色器中输出的顶点组装成指定的图元,直觉上对应物体的 “面”:
loading...
上图没有列出全部的图元装配模式,在本例里只需要三角形 TRIANGLES 即可

光栅化 (Rasterization)

将装配好的矢量的图元离散化/像素化,变成由像素组成的图元,这个过程称为光栅化,具体来说:
  • 管线在光栅化过程中使用了一些光栅化的算法,选择出在图元边界所包围的像素点,最终组成片元。常用的光栅化算法如Bresenham光栅化算法等。
  • 如果在顶点着色器中的顶点中设置例如颜色,法线等属性,在光栅化过程对基于顶点对各个像素点做线性插值。

片段着色器 (Fragment shader)

进一步处理上一步光栅化得到的图片像素片段

混合和测试 (Testing and blending)

这一步会将片段着色器传过来的片段做测试,测试失败的像素直接删除,测试成功的点去保留,并最终渲染到屏幕上。
常见的测试主要包括模版测试和深度测试,通过深度测试可以实现物体面的前后顺序,具体不展开了,我这里的白噪声不需要这个。

# 快速入门 GLSL 编写着色器

着色器 (shader) 是一种 GPU 程序, 由 GPU 负责执行。GLSL 即 OpenGL Shading Language, 可以用来编写着色器。
GLSL 采用了类 C 的语法,并提供了若干方便的数据结构和选择器,通过 uniform 全局变量将相关参数传入 GLSL 上下文中。
这里以本例子里用到的顶点着色器为例说明
00attribute vec4 position;
01void main() {
02  gl_Position = position;
03}

main 函数

你懂的, 执行入口

vec4 是什么

vec4 是一种数据结构,表示一个四维向量, 你可以将它想象为这个结构:
00interface vec4 {
01  x: number;
02  y: number;
03  z: number;
04  w: number; // 远近
05}
此外还有 vec3, vec2, 这两个是在 vec4 上减去 w 和 z 得到。
这里提一下选择器这个概念: (语法糖)
00// 构造一个 vec4 存储在 a 变量中
01vec4 a = vec4(1.0, 2.0, 3.0, 4.0);
02vec3 b = a.xyz; // (1, 2, 3)
03vec2 c = a.xy; // (1, 2)
04vec2 d = a.yz; // (2, 1) 选择 y z 维并作为 vec2 返回
可以通过在变量名后面接 .xyz 的方式快速选择其中的几个维度出来

attribute 属性

attribute 标记的变量说明这个变量的取值是从 "缓冲" 取的
具体来说,缓冲是是 js 这边发送到 GPU 的一段二进制数据序列,通常情况下缓冲数据包括位置,法向量,纹理坐标,顶点颜色值等,除此之外,缓冲还可以存储任何数据,因为它本质就是一段 buffer
而具体到如何从 "缓冲" 得到 "vec4" 这件事,则需要通过 js 进行定义:
00const canvas = document.createElement('canvas')!;
01const gl = canvas.getContext('webgl')!;
02
03// 开辟一块缓冲
04const positionBuffer = gl.createBuffer();
05gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
06
07// 取得访问到 position 变量的 location
08const positionAttributeLocation = gl.getAttribLocation(
09  program, // program 为 glsl 编译后的实例, 这里先忽略
10  'position' // 对应 glsl 程序里的 position 变量名
11);
12
13// 用 ts 定义顶点数据 (x,y,z) 一共 3 个点, 数组长度为 9 
14const vertices: number[] = [
15  -1,  1,  0,
16  -1, -1,  0,
17   1, -1,  0,
18];
19
20// 写入到缓冲 buffer 中
21gl.bufferData(
22  gl.ARRAY_BUFFER,            // 表明是 ARRAY_BUFFER
23  new Float32Array(vertices), // 构造为 Float32Array 格式
24  gl.STATIC_DRAW,             // 我们不会经常改这个顶点数据
25);
26
27// 启用 position 属性
28gl.enableVertexAttribArray(positionAttributeLocation);
29
30// 告诉如何从缓冲中取值并写到 position 变量上
31gl.vertexAttribPointer(
32  positionAttributeLocation,
33  3, // vertices 数组中每三个赋值到 vec4 position 上
34     // 因此会少一个 w, glsl 会补一个默认值 0
35  gl.FLOAT, // 浮点类型
36  false,    // 不需要归一化数据
37  0,        // 每次迭代运行运动多少内存到下一个数据开始点
38            // 这里 0 代表 单位数量 * 每个单位占用内存 sizeof(type)
39  0         // 从缓冲起始位置开始读取
40);
可以看见,一般的做法是利用 js 定义顶点数据传入缓冲,并定义属性获取方式,通常定义为三个三个的获取,最后在顶点着色器中就可以使用 position 变量了

gl_Position

通过赋值内置变量 gl_Position 来告诉外部本次顶点变换后的结果, 在这里可以看见我们没有做任何顶点变换,直接将从缓冲得到的顶点透传出去了。

# WenGL 绘制思路

根据前文提到的渲染管线进行思考,用三个顶点绘制出三角形,然后利用片段着色器随机填充三角形即可
loading...
你可能会问三角形不对,那么可以再加一个三角形构成一个正方形。

# 白噪声顶点着色器

因为不需要什么特殊的顶点处理,所以顶点着色器只需要从缓冲中取得对应点即可
00attribute vec4 position;
01void main() {
02  gl_Position = position;
03}

# 白噪声片段着色器

00// 定义片段着色器
01#ifdef GL_ES
02precision mediump float;
03#endif
04
05uniform float u_rand;
06uniform float u_size;
07
08// 随机输出一个在 0 和 1 之间的数
09// 这是一个很有意思的随机数生成,
10// 具体原理可以 google 下
11float random2d(vec2 co) {
12  float a = 12.9898;
13  float b = 78.233;
14  float c = 43758.5453;
15  // dot 为点乘
16  // dt = (co.x * a) + (co.y * b)
17  float dt = dot(co.xy, vec2(a,b));
18  float sn = mod(dt, 3.14);
19  return fract(sin(sn) * c);
20}
21
22// 将 x 定在 x - (x % u_size) 区间内
23float round(float x) {
24  return x - mod(x, u_size);
25}
26
27void main(){
28  // gl_FragCoord 代表当前处理的片元坐标
29  // round函数为 四舍五入
30  vec2 coord = vec2(
31    round(gl_FragCoord.x),
32    round(gl_FragCoord.y)
33  );
34
35  float num = random2d(
36    vec2(
37      coord.x * u_rand,
38      coord.y * u_rand
39    )
40  );
41
42  // num 大于 0.5 的话归到 1.0 否则归到 0
43  float p = (num > 0.5) ? 1.0 : 0.0;
44
45  // 构成一个 vec4 并通过 gl_FragColor 输出 (rgba 四个值)
46  gl_FragColor = vec4(p, p, p, 1.0);
47}

# WebGL 编程

000import shaderVertexSource from './shader-vertex-source.glsl';
001import shaderFragmentSource from './shader-fragment-source.glsl';
002
003export class WhiteNoiseWebgl {
004  private stop = true;
005  private inited = false;
006  private canvas: HTMLCanvasElement | null = null;
007  private gl: WebGLRenderingContext | null = null;
008  private uSize = 4;
009
010  public play = () => {
011    this.stop = !this.stop;
012  }
013
014  public init = (canvas: HTMLCanvasElement) => {
015    this.stop = true;
016    this.inited = true;
017    this.canvas = canvas;
018    const gl = canvas.getContext('webgl')!;
019    this.gl = gl;
020
021    // 视口
022    gl.viewport(0, 0, canvas.width, canvas.height);
023
024    // 创建并编译顶点着色器
025    const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
026    gl.shaderSource(vertexShader, shaderVertexSource);
027    gl.compileShader(vertexShader); // 编译
028
029    // 编译错误的话打 log
030    if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
031      console.error(gl.getShaderInfoLog(vertexShader));
032    }
033
034    // 创建并编译片段着色器
035    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
036    gl.shaderSource(fragmentShader, shaderFragmentSource);
037    gl.compileShader(fragmentShader); // 编译
038
039    // 编译错误的话打 log
040    if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
041      console.error(gl.getShaderInfoLog(fragmentShader));
042    }
043
044    // 创建程序, 并将两个着色器放上去, 最后跟 gl 上下文绑定
045    const program = gl.createProgram()!;
046    gl.attachShader(program, vertexShader);
047    gl.attachShader(program, fragmentShader);
048    gl.linkProgram(program);
049
050    // 错误处理
051    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
052      console.error(gl.getProgramInfoLog(program));
053    }
054
055    // 启用 program
056    gl.useProgram(program);
057
058    // 获取顶点着色器的位置属性
059    const positionAttributeLocation = gl.getAttribLocation(program, 'position');
060    const positionBuffer = gl.createBuffer();
061    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
062
063    // 定义顶点数据
064    const vertices: number[] = [
065      -1,  1,  0, // 第一个三角形
066      -1, -1,  0,
067       1, -1,  0,
068
069       1, -1,  0, // 第二个三角形
070       1,  1,  0,
071      -1,  1,  0,
072    ];
073
074    // 写入到缓冲 buffer 中
075    gl.bufferData(
076      gl.ARRAY_BUFFER,            // 表明是 ARRAY_BUFFER
077      new Float32Array(vertices), // 构造为 Float32Array 格式
078      gl.STATIC_DRAW,             // 我们不会经常改这个顶点数据
079    );
080    
081    // 启用 position 属性
082    gl.enableVertexAttribArray(positionAttributeLocation);
083
084    // 告诉如何从缓冲中取值并写到 position 变量上
085    gl.vertexAttribPointer(
086      positionAttributeLocation,
087      3, // vertices 数组中每三个赋值到 vec4 position 上
088         // 因此会少一个 w, glsl 会补一个默认值 0
089      gl.FLOAT, // 浮点类型
090      false,    // 不需要归一化数据
091      0,        // 每次迭代运行运动多少内存到下一个数据开始点
092                // 这里 0 代表 单位数量 * 每个单位占用内存 sizeof(type)
093      0         // 从缓冲起始位置开始读取
094    );
095
096    // 获取 uniform 变量的位置
097    // 通过这种方式可以访问到片段着色器里的
098    // u_rand 和 u_size 这两个全局变量
099    const randUniformLocation = gl.getUniformLocation(
100      program, 'u_rand'
101    )!;
102    const sizeUniformLocation = gl.getUniformLocation(
103      program, 'u_size'
104    )!;
105
106    // 绘制一帧
107    const render = () => {
108      // 传递 u_rand 参数到着色器
109      gl.uniform1f(randUniformLocation, Math.random());
110
111      // 传递 u_size 参数到着色器
112      gl.uniform1f(sizeUniformLocation, this.uSize);
113  
114      // 清空画布
115      gl.clearColor(0, 0, 0, 1);
116      gl.clear(gl.COLOR_BUFFER_BIT);
117
118      gl.drawArrays(
119        gl.TRIANGLES, // gl.TRIANGLES 方式进行图元组合
120        0, // 从 0 开始
121        6  // 6 个顶点
122      );
123    }
124
125    const animate = () => {
126      // deinit 结束后动画要暂停
127      if (!this.inited) return;
128
129      if (this.stop) {
130        setTimeout(animate, 50);
131        return;
132      }
133
134      render();
135      requestAnimationFrame(animate);
136    }
137
138    render();
139    animate();
140  }
141
142  public deinit = () => {
143    this.inited = true;
144    this.stop = true;
145    this.canvas = null;
146    this.gl = null;
147  }
148}
最后 new WhiteNoiseWebgl() 并调用 init 即可
00import { whiteNoiseWebgl } from './white-noise-webgl';
01
02const canvas = document.createElement('canvas')!
03document.body.appendChild(canvas);
04canvas.width = window.innerWidth / 2;
05canvas.height = window.innerHeight / 2;
06
07const instance = new whiteNoiseWebgl();
08instance.init(canvas);
播放

# 参考





回到顶部