如何在着色器中实现缓动函数与非线性插值
实现轨迹动画
<!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"> import { Animator } from "./common/lib/animator/index.js"; const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; uniform vec2 translation; void main() { gl_PointSize = 1.0; vUv = uv; mat3 transformMatrix = mat3( 1, 0, 0, 0, 1, 0, translation, 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.translation = [-0.5, 0]; const animator = new Animator({ duration: 2000 }); animator.animate(renderer, ({ target, timing }) => { target.uniforms.translation = [ -0.5 * (1 - timing.p) + 0.5 * timing.p, 0, ]; }); renderer.setMeshData([ { positions: [ [-0.25, -0.25], [-0.25, 0.25], [0.25, 0.25], [0.25, -0.25], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], }, cells: [ [0, 1, 2], [2, 0, 3], ], }, ]); renderer.render(); </script> </body> </html>
实现缓动效果的轨迹动画
下面我们不使用 Animator,而是直接将时间 uTime 参数传入 Shader,然后在 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>实现缓动效果的轨迹动画</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 vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; uniform vec4 uFromTo; uniform float uTime; // 缓动函数 float easing(in float p) { // return smoothstep(0.0, 1.0, p); // return clamp(p * p, 0.0, 1.0); return clamp(p * (2.0 - p), 0.0, 1.0); } void main() { gl_PointSize = 1.0; vUv = uv; vec2 from = uFromTo.xy; vec2 to = uFromTo.zw; float p = easing(uTime / 2.0); vec2 translation = mix(from, to, p); mat3 transformMatrix = mat3( 1, 0, 0, 0, 1, 0, translation, 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.uTime = 0; renderer.uniforms.uFromTo = [-0.5, 0, 0.5, 0]; let startTime = null; requestAnimationFrame(function update() { startTime = startTime || Date.now(); renderer.uniforms.uTime = (Date.now() - startTime) / 1000; requestAnimationFrame(update); }); renderer.setMeshData([ { positions: [ [-0.25, -0.25], [-0.25, 0.25], [0.25, 0.25], [0.25, -0.25], ], 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 type="module"> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; attribute vec4 color; varying vec2 vUv; varying vec4 vColor; uniform vec4 uFromTo; uniform float uTime; void main() { gl_PointSize = 1.0; vUv = uv; vColor = color; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif varying vec2 vUv; varying vec4 vColor; void main() { gl_FragColor = vColor; } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); renderer.setMeshData([ { positions: [ [-0.5, -0.25], [-0.5, 0.25], [0.5, 0.25], [0.5, -0.25], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ], color: [ [1, 0, 0, 1], [1, 0, 0, 1], [0, 0.5, 0, 1], [0, 0.5, 0, 1], ], }, cells: [ [0, 1, 2], [2, 0, 3], ], }, ]); renderer.render(); </script> </body> </html>
非线性的颜色过渡
在片元着色器中可以采用 uniform 的方式,用 easing 来实现非线性的插值。
<!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 vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; uniform float uTime; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif float easing(in float p) { // return smoothstep(0.0, 1.0, p); // return clamp(p * p, 0.0, 1.0); return clamp(p * (2.0 - p), 0.0, 1.0); } varying vec2 vUv; uniform vec4 fromColor; uniform vec4 toColor; void main() { float d = easing(vUv.x); gl_FragColor = mix(fromColor, toColor, d); } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); renderer.uniforms.fromColor = [1, 0, 0, 1]; renderer.uniforms.toColor = [0, 0.5, 0, 1]; renderer.setMeshData([ { positions: [ [-0.5, -0.25], [-0.5, 0.25], [0.5, 0.25], [0.5, -0.25], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ] }, cells: [ [0, 1, 2], [2, 0, 3], ], }, ]); renderer.render(); </script> </body> </html>
贝塞尔曲线插值色带
可以参考:http://flong.com/archive/texts/code/shapers_bez/index.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 type="module"> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; uniform float uTime; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif // http://flong.com/archive/texts/code/shapers_bez/index.html // Helper functions: float slope_from_t (float t, float A, float B, float C){ float dtdx = 1.0/(3.0*A*t*t + 2.0*B*t + C); return dtdx; } float x_from_t (float t, float A, float B, float C, float D){ float x = A*(t*t*t) + B*(t*t) + C*t + D; return x; } float y_from_t (float t, float E, float F, float G, float H){ float y = E*(t*t*t) + F*(t*t) + G*t + H; return y; } float cubic_bezier (float x, float a, float b, float c, float d){ float y0a = 0.00; // initial y float x0a = 0.00; // initial x float y1a = b; // 1st influence y float x1a = a; // 1st influence x float y2a = d; // 2nd influence y float x2a = c; // 2nd influence x float y3a = 1.00; // final y float x3a = 1.00; // final x float A = x3a - 3.0 *x2a + 3.0 * x1a - x0a; float B = 3.0 * x2a - 6.0 * x1a + 3.0 * x0a; float C = 3.0 * x1a - 3.0 * x0a; float D = x0a; float E = y3a - 3.0 * y2a + 3.0 * y1a - y0a; float F = 3.0 * y2a - 6.0 * y1a + 3.0 * y0a; float G = 3.0 * y1a - 3.0 * y0a; float H = y0a; // Solve for t given x (using Newton-Raphelson), then solve for y given t. // Assume for the first guess that t = x. float currentt = x; const int nRefinementIterations = 5; for (int i=0; i < nRefinementIterations; i++){ float currentx = x_from_t(currentt, A,B,C,D); float currentslope = slope_from_t(currentt, A,B,C); currentt -= (currentx - x)*(currentslope); currentt = clamp(currentt, 0.0, 1.0); } float y = y_from_t(currentt, E,F,G,H); return y; } 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); } varying vec2 vUv; void main() { // float d = vUv.x; float d = cubic_bezier(vUv.x, 0.5, -1.5, 0.5, 2.5); gl_FragColor.rgb = hsb2rgb(vec3(d, 1.0, 1.0)); 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: [ [-0.5, -0.25], [-0.5, 0.25], [0.5, 0.25], [0.5, -0.25], ], attributes: { uv: [ [0, 0], [0, 1], [1, 1], [1, 0], ] }, cells: [ [0, 1, 2], [2, 0, 3], ], }, ]); renderer.render(); </script> </body> </html>
如何在片元着色器中实现随机粒子动画
可以使用随机 + 噪声来实现一个粒子效果。
首先,我们设置随机数用来生成距离场的初始值,然后设置噪声用来形成位移,最后传入 uTime 变量来实现动画。
<!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; attribute vec4 color; 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 highp float random(vec2 co) { highp float a = 12.9898; highp float b = 78.233; highp float c = 43758.5453; highp float dt= dot(co.xy ,vec2(a,b)); highp float sn= mod(dt,3.14); return fract(sin(sn) * c); } // Value Noise by Inigo Quilez - iq/2013 // https://www.shadertoy.com/view/lsf3WH highp float noise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); vec2 u = f * f * (3.0 - 2.0 * f); return mix( mix( random( i + vec2(0.0,0.0) ), random( i + vec2(1.0,0.0) ), u.x), mix( random( i + vec2(0.0,1.0) ), random( i + vec2(1.0,1.0) ), u.x), u.y ); } float sdf_circle(vec2 st, vec2 c, float r) { return 1.0 - length(st - c) / r; } varying vec2 vUv; uniform float uTime; void main() { vec2 st = vUv; float rx = mix(-0.2, 0.2, noise(vec2(7881.32, 0) + random(st) + uTime)); float ry = mix(-0.2, 0.2, noise(vec2(0, 1433.59) + random(st) + uTime)); float dis = distance(st, vec2(0.5)); dis = pow((1.0 - dis), 2.0); float d = sdf_circle(st + vec2(rx, ry), vec2(0.5), 0.2); d = smoothstep(0.0, 0.1, d); gl_FragColor = vec4(dis * d * vec3(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.uTime = 0; requestAnimationFrame(function update(t) { renderer.uniforms.uTime = 0.001 * t; requestAnimationFrame(update); }); 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>