说明
【跟月影学可视化】学习笔记。
如何用着色器实现固定帧动画
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <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 sampler2D tMap; uniform float fWidth; uniform vec2 vFrames[3]; uniform int frameIndex; void main() { vec2 uv = vUv; // 动画只有 3 帧,所以最多只需要循环 3 次。 for (int i = 0; i < 3; i++) { // vFrames 是每一帧动画的图片起始 x 和结束 x 坐标 uv.x = mix(vFrames[i].x, vFrames[i].y, vUv.x) / fWidth; if(float(i) == mod(float(frameIndex), 3.0)) break; } vec4 color = texture2D(tMap, uv); gl_FragColor = color; } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const textureURL = "./assets/img/bird.png"; (async function () { const texture = await renderer.loadTexture(textureURL); const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); renderer.uniforms.tMap = texture; renderer.uniforms.fWidth = 272; // 图片的总宽度 renderer.uniforms.vFrames = [2, 88, 90, 176, 178, 264]; renderer.uniforms.frameIndex = 0; setInterval(() => { renderer.uniforms.frameIndex++; }, 200); const x = 43 / canvas.width; const y = 30 / canvas.height; renderer.setMeshData([ { positions: [ [-x, -y], [-x, y], [x, y], [x, -y], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, cells: [ [0, 1, 2], [2, 0, 3], ], }, ]); renderer.render(); })(); </script> </body> </html>
如何用着色器实现非固定帧动画
WebGL 有两种 Shader,分别是顶点着色器和片元着色器,它们都可以用来实现动画。在绘制一帧画面的时候,顶点着色器的运算量会大大少于片元着色器,所以使用顶点着色器消耗的性能更少。片元着色器可以使用重复、随机、噪声等技巧来绘制更加复杂的效果。
用顶点着色器实现非固定帧动画
在顶点着色器中,我们直接改变了顶点坐标,所以这样实现的旋转动画和 WebGL 坐标系(右手系)的方向一致,角度增大呈逆时针方向旋转。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <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; uniform float rotation; void main() { gl_PointSize = 1.0; vUv = uv; float c = cos(rotation); float s = sin(rotation); mat3 transformMatrix = mat3( c, s, 0, -s, c, 0, 0, 0, 1 ); vec3 pos = transformMatrix * vec3(a_vertexPosition, 1); gl_Position = vec4(pos, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif varying vec2 vUv; uniform vec4 color; void main() { gl_FragColor = color; } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); renderer.uniforms.color = [250/255, 128/255, 114/255, 1]; renderer.uniforms.rotation = 0.0; renderer.setMeshData([ { positions: [ [-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, cells: [ [0, 1, 2], [2, 0, 3], ], }, ]); renderer.render(); function update() { renderer.uniforms.rotation += 0.05; requestAnimationFrame(update); } update(); </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 vec4 color; uniform float rotation; void main() { vec2 st = 2.0 * (vUv - vec2(0.5)); float c = cos(rotation); float s = sin(rotation); mat3 transformMatrix = mat3( c, s, 0, -s, c, 0, 0, 0, 1 ); vec3 pos = transformMatrix * vec3(st, 1.0); float d1 = 1.0 - smoothstep(0.5, 0.505, abs(pos.x)); float d2 = 1.0 - smoothstep(0.5, 0.505, abs(pos.y)); gl_FragColor = d1 * d2 * color; } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); renderer.uniforms.color = [250/255, 128/255, 114/255, 1]; renderer.uniforms.rotation = 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(); function update() { renderer.uniforms.rotation += 0.05; requestAnimationFrame(update); } update(); </script> </body> </html>
绘制大量重复的旋转正方形
利用网格实现了大量的重复动画,充分利用了 GPU 的并行效率,比用其他方式把图形一个一个地绘制出来性能要高得多。
<!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 rotation; float random (vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233)))*43758.5453123); } vec3 hsb2rgb(vec3 c){ vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0), 6.0)-3.0)-1.0, 0.0, 1.0); rgb = rgb * rgb * (3.0 - 2.0 * rgb); return c.z * mix(vec3(1.0), rgb, c.y); } void main() { vec2 f_uv = fract(vUv * 10.0); vec2 i_uv = floor(vUv * 10.0); vec2 st = 2.0 * (f_uv - vec2(0.5)); float c = 0.7 * cos(rotation); float s = 0.7 * sin(rotation); mat3 transformMatrix = mat3( c, s, 0, -s, c, 0, 0, 0, 1 ); vec3 pos = transformMatrix * vec3(st, 1.0); float d1 = 1.0 - smoothstep(0.5, 0.505, abs(pos.x)); float d2 = 1.0 - smoothstep(0.5, 0.505, abs(pos.y)); gl_FragColor = d1 * d2 * vec4(hsb2rgb(vec3(random(i_uv), 1.0, 1.0)), 1.0); } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); renderer.uniforms.rotation = 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(); function update() { renderer.uniforms.rotation += 0.05; requestAnimationFrame(update); } update(); </script> </body> </html>