说明
【跟月影学可视化】学习笔记。
如何绘制大批量重复图案
比如绘制带有网格背景的画布
如果将网格绘制在 Canvas2D 画布上,网格的线条很多,重绘消耗系统的性能。
方案一:使用 background-image 来绘制重复图案
因为浏览器将渐变属性视为图片,所以可以将渐变设置在任何可以接受图片的 CSS 属性上
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>使用 background-image 来绘制重复图案</title> <style> canvas { border: 1px dashed salmon; background-image: linear-gradient(to right, transparent 90%, #ccc 0), linear-gradient(to bottom, transparent 90%, #ccc 0); background-size: 8px 8px, 8px 8px; } </style> </head> <body> <canvas width="512" height="512"></canvas> </body> </html>
不足之处:
- 和直接绘制在画布上的其他图形就处于不同的层,没法将它覆盖在这些图形上
- 用坐标变换来缩放或移动元素时,作为元素背景的网格是不会随着缩放或移动而改变的。
方案二:使用 Shader 来绘制重复图案
利用 GPU 并行计算的特点,使用着色器来绘制背景网格这样的重复图案。使用 Shader 绘制重复图案,不管绘制多么细腻,图案重复多少次,绘制消耗的时间几乎是常量,不会遇到性能瓶颈。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>使用 Shader 来绘制重复图案</title> <style> canvas { border: 1px dashed salmon; } </style> </head> <body> <canvas width="512" height="512"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script type="module"> const canvas = document.querySelector('canvas'); // 这里使用基础库 gl-renderer 库,它在 WebGL 底层的基础上进行了一些简单的封装 const renderer = new GlRenderer(canvas); console.log(renderer); // 顶点着色器 const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; /** * 片元着色器:渲染过程 * 1、获得重复的 rows 行 rows 列的值 st * fract:用来获取一个数的小数部分 * vUv:由顶点着色器传来的 uv 属性(纹理坐标)乘上 rows 值 * 2、step 函数是 Shader 中另一个很常用的函数,它就是一个阶梯函数。 * 它的原理是:当 step(a, b) 中的 b < a 时,返回 0;当 b >= a 时,返回 1。 * 3、mix 是线性插值函数,mix(a, b, c) 表示根据 c 是 0 或 1,返回 a 或者 b。 * vec3(1.0)是白色,vec3(0.8)是灰色 * */ const fragment = ` #ifdef GL_ES precision mediump float; #endif varying vec2 vUv; uniform float rows; void main() { vec2 st = fract(vUv * rows); float d1 = step(st.x, 0.9); float d2 = step(0.1, st.y); gl_FragColor.rgb = mix(vec3(0.8), vec3(1.0), d1 * d2); gl_FragColor.a = 1.0; } `; // 加载片元着色器并创建程序 const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); // 设置 uniform 变量,rows:表示每一行显示多少个网格。 renderer.uniforms.rows = 64; // 将顶点数据送入缓冲区 renderer.setMeshData([{ // positions:顶点,这四个顶点坐标正好覆盖了整个 Canvas 画布 positions: [ [-1, -1], [-1, 1], [1, 1], [1, -1], ], attributes: { // uv:纹理坐标,这个坐标系的左下角为 0,0,右上角为 1,1 uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, // cells:顶点索引,这个矩形画布剖分成两个三角形,顶点下标分别是 (0, 1, 2) 和 (2, 0, 3)。 cells: [[0, 1, 2], [2, 0, 3]], }]); // 渲染 renderer.render(); </script> </body> </html>
顶点坐标和 uv(纹理)坐标:
y = fract(x)
在整数区间内周期重复示意图:
Step 函数:
效果如下:
如何绘制分形图案
一个分形图案可以划分成无数个部分,而每个部分的形状又都和这个图案整体具有相似性。
比如:自然界中的分形——罗马花椰菜
分形公式(曼德勃罗特集):( Z n {Z}_{n} Zn 和 Z n + 1 {Z}_{n+1} Zn+1是复数,C 是一个实数常量。)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>如何绘制分形图案</title> <style> canvas { border: 1px dashed salmon; } </style> </head> <body> <canvas width="512" height="512"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script type="module"> const canvas = document.querySelector('canvas'); // 这里使用基础库 gl-renderer 库,它在 WebGL 底层的基础上进行了一些简单的封装 const renderer = new GlRenderer(canvas); console.log(renderer); // 顶点着色器 const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; // 曼德勃罗特集是无限迭代的,给一个足够精度的最大迭代次数,比如:65536 const fragment = ` #ifdef GL_ES precision mediump float; #endif varying vec2 vUv; uniform vec2 center; uniform float scale; uniform int iterations; vec2 f(vec2 z, vec2 c) { return mat2(z, -z.y, z.x) * z + c; } vec3 palette(float t, vec3 c1, vec3 c2, vec3 c3, vec3 c4) { float x = 1.0 / 3.0; if (t < x) return mix(c1, c2, t/x); else if (t < 2.0 * x) return mix(c2, c3, (t - x)/x); else if (t < 3.0 * x) return mix(c3, c4, (t - 2.0*x)/x); return c4; } void main() { vec2 uv = vUv; vec2 c = center + 4.0 * (uv - vec2(0.5)) / scale; vec2 z = vec2(0.0); bool escaped = false; int j; for (int i = 0; i < 65536; i++) { if(i > iterations) break; j = i; z = f(z, c); if (length(z) > 2.0) { escaped = true; break; } } gl_FragColor.rgb = escaped ? max( 1.0, log(scale)) * palette(float(j)/ float(iterations), vec3(0.02, 0.02, 0.03), vec3(0.1, 0.2, 0.3), vec3(0.0, 0.3, 0.2), vec3(0.0, 0.5, 0.8) ) : vec3(0.0); gl_FragColor.a = 1.0; } `; // 加载片元着色器并创建程序 const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); renderer.uniforms.center = [0.367, 0.303]; renderer.uniforms.scale = 1; renderer.uniforms.iterations = 256; // 将顶点数据送入缓冲区 renderer.setMeshData([{ // positions:顶点,这四个顶点坐标正好覆盖了整个 Canvas 画布 positions: [ [-1, -1], [-1, 1], [1, 1], [1, -1], ], attributes: { // uv:纹理坐标,这个坐标系的左下角为 0,0,右上角为 1,1 uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, // cells:顶点索引,这个矩形画布剖分成两个三角形,顶点下标分别是 (0, 1, 2) 和 (2, 0, 3)。 cells: [[0, 1, 2], [2, 0, 3]], }]); // 渲染 renderer.render(); function update() { const factor = Math.max(0.1, Math.log(renderer.uniforms.scale)); renderer.uniforms.scale = (renderer.uniforms.scale += factor) % 10000; renderer.uniforms.iterations = factor * 500; requestAnimationFrame(update); } setTimeout(update, 2000); </script> </body> </html>
如何给图案增加随机效果
伪随机函数的原理是,取正弦函数偏后部的小数部分的值来模拟随机。如果我们传入一个确定的 st 值,它就会返回一个符合随机分布的确定的 float 值。
重复网格
可以用 floor 取整函数,来生成随机的色块。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>重复网格</title> <style> canvas { border: 1px dashed salmon; } </style> </head> <body> <canvas width="512" height="512"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif varying vec2 vUv; float random (vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); } void main() { vec2 st = vUv * 10.0; gl_FragColor.rgb = vec3(random(floor(st))); gl_FragColor.a = 1.0; } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); // 加载片元着色器并创建程序 const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); // 将顶点数据送入缓冲区 renderer.setMeshData([ { positions: [ [-1, -1], [-1, 1], [1, 1], [1, -1], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, cells: [ [0, 1, 2], [2, 0, 3], ], }, ]); // 渲染 renderer.render(); </script> </body> </html>
网格移动
结合随机和动态效果的网格
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>结合随机和动态效果的网格</title> <style> canvas { border: 1px dashed salmon; } </style> </head> <body> <canvas width="512" height="512"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif varying vec2 vUv; uniform float uTime; float random (vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233)))* 43758.5453123); } void main() { vec2 st = vUv * vec2(100.0, 50.0); st.x -= (1.0 + 10.0 * random(vec2(floor(st.y)))) * uTime; vec2 ipos = floor(st); // integer vec2 fpos = fract(st); // fraction vec3 color = vec3(step(random(ipos), 0.7)); color *= step(0.2,fpos.y); gl_FragColor.rgb = color; gl_FragColor.a = 1.0; } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); // 加载片元着色器并创建程序 const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); renderer.uniforms.uTime = 0.0; // 将顶点数据送入缓冲区 renderer.setMeshData([ { positions: [ [-1, -1], [-1, 1], [1, 1], [1, -1], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, cells: [ [0, 1, 2], [2, 0, 3], ], }, ]); // 渲染 renderer.render(); requestAnimationFrame(function update(t) { renderer.uniforms.uTime = 4 * t / 1000; requestAnimationFrame(update); }); </script> </body> </html>
迷宫
在 Shader 中用 smoothstep 函数生成可以随机旋转方向的线段生成迷宫。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>迷宫</title> <style> canvas { border: 1px dashed salmon; } </style> </head> <body> <canvas width="512" height="512"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision mediump float; #endif #define PI 3.14159265358979323846 varying vec2 vUv; uniform vec2 u_resolution; uniform int rows; float random (in vec2 _st) { return fract(sin(dot(_st.xy, vec2(12.9898,78.233))) * 43758.5453123); } vec2 truchetPattern(in vec2 _st, in float _index){ _index = fract(((_index-0.5)*2.0)); if (_index > 0.75) { _st = vec2(1.0) - _st; } else if (_index > 0.5) { _st = vec2(1.0-_st.x,_st.y); } else if (_index > 0.25) { _st = 1.0-vec2(1.0-_st.x,_st.y); } return _st; } void main() { vec2 st = vUv * float(rows); vec2 ipos = floor(st); // integer vec2 fpos = fract(st); // fraction vec2 tile = truchetPattern(fpos, random( ipos )); float color = 0.0; color = smoothstep(tile.x-0.3,tile.x,tile.y) - smoothstep(tile.x,tile.x+0.3,tile.y); gl_FragColor = vec4(vec3(color),1.0); } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); // 加载片元着色器并创建程序 const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); renderer.uniforms.rows = 20; // 将顶点数据送入缓冲区 renderer.setMeshData([ { positions: [ [-1, -1], [-1, 1], [1, 1], [1, -1], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, cells: [ [0, 1, 2], [2, 0, 3], ], }, ]); // 渲染 renderer.render(); </script> </body> </html>
【The Book of Shaders】
这是一本关于 Fragment Shaders(片段着色器)的入门指南,它将一步一步地带你领略其中的纷繁与抽象。
比如上面提到的 smoothstep 函数