本文介绍白噪声生成, 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);