WebGL——水波纹特效
大家好,今天我ccentry要做一个水波纹特效,我们来看看水波纹特效的做法。首先我们来看一下水波纹特效的效果是怎么样的,请看下图。
我们要做的就是类似这种纹理特效,那么我们来看看是如何制作的吧。首先鲫鱼我新建一个空项目,来编写这个demo,项目结构如下图所示。
img文件夹中存放的是uv贴图和底图,js文件夹下存放的是jquery和我的水波纹效果的js文件,还有就是展示页面index.html。很简单,没什么东西了,接下来就来看鲫鱼我是怎么实现上面这个水波纹特效的吧。我们先开始编辑ripples.js,这就是这个特效的核心功能。再来看一遍工程结构。
且看鲫鱼我怎么写这个ripple.js。首先我们检查浏览器是否支持webgl的api,这是一个浏览器校验功能函数,代码块如下。
var gl; function hasWebGLSupport() { var canvas = document.createElement('canvas'); var context = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); var result = context && context.getExtension('OES_texture_float') && context.getExtension('OES_texture_float_linear'); return result; }
这里先是创建了一个canvas标签,然后由这个canvas中拿到context('webgl')对象,我们所有做webgl的api都由此而来。这里只是校验并获取gl对象的动作,然后我们来看函数的返回值,这里有个js的&&运算符的妙用,result = context && context.getExtension('OES_texture_float') && context.getExtension('OES_texture_float_linear');这句话是指context存在,就取context.getExtension('OES_texture_float'),而且context.getExtension('OES_texture_float')存在,就取context.getExtension('OES_texture_float_linear'),这是递进式的取值方式,我们最终通过了前两个的校验获取到了'OES_texture_float_linear'对象,鲫鱼相信大家也都看得懂,我们就不在这里浪费时间了,继续往下看。
var supportsWebGL = hasWebGLSupport();
这句就一目了然了,我们调用了hasWebGLSupport函数,从而获取到了'OES_texture_float_linear'线性浮点数的材质处理对象。这个对象我们后面要使用到,现在这里拿到手再说。我们接下去看。
function createProgram(vertexSource, fragmentSource, uniformValues) { function compileSource(type, source) { var shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { throw new Error('compile error: ' + gl.getShaderInfoLog(shader)); } return shader; } var program = {}; program.id = gl.createProgram(); gl.attachShader(program.id, compileSource(gl.VERTEX_SHADER, vertexSource)); gl.attachShader(program.id, compileSource(gl.FRAGMENT_SHADER, fragmentSource)); gl.linkProgram(program.id); if (!gl.getProgramParameter(program.id, gl.LINK_STATUS)) { throw new Error('link error: ' + gl.getProgramInfoLog(program.id)); } // Fetch the uniform and attribute locations program.uniforms = {}; program.locations = {}; gl.useProgram(program.id); gl.enableVertexAttribArray(0); var name, type, regex = /uniform (\w+) (\w+)/g, shaderCode = vertexSource + fragmentSource; while ((match = regex.exec(shaderCode)) != null) { name = match[2]; program.locations[name] = gl.getUniformLocation(program.id, name); } return program; }
这一个功能自然不必再解释了,这是gl对象绑定shader的工具,参数为vertexSource顶点着色器字符串,fragmentSource片段着色器字符串,uniformValues向着色器传递的参数,如果读者对这个函数存在疑问,鲫鱼建议读者先学习《webgl编程指南》和阅读鲫鱼之前的博客https://www.cnblogs.com/ccentry/p/9864847.html,这里不再赘述。我们再往下看。
function bindTexture(texture, unit) { gl.activeTexture(gl.TEXTURE0 + (unit || 0)); gl.bindTexture(gl.TEXTURE_2D, texture); }
这个函数是绑定材质的函数,参数texture是材质,unit是偏移量,这个函数只有两步操作,启用gl.TEXTURE0材质类型,将传入的材质按照该类型去绑定到gl程序对象上。该函数做到这件事,我们看下去。
//Ripples构造函数 var Ripples = function (el, options) { var that = this; this.$el = $(el); this.$el.addClass('jquery-ripples'); // If this element doesn't have a background image, don't apply this effect to it var backgroundUrl = (/url\(["']?([^"']*)["']?\)/.exec(this.$el.css('background-image'))); if (backgroundUrl == null) return; backgroundUrl = backgroundUrl[1]; this.interactive = options.interactive; this.resolution = options.resolution || 256; this.textureDelta = new Float32Array([1 / this.resolution, 1 / this.resolution]); this.perturbance = options.perturbance; this.dropRadius = options.dropRadius; var canvas = document.createElement('canvas'); canvas.width = this.$el.innerWidth(); canvas.height = this.$el.innerHeight(); this.canvas = canvas; this.$canvas = $(canvas); this.$canvas.css({ position: 'absolute', left: 0, top: 0, right: 0, bottom: 0, zIndex: -1 }); this.$el.append(canvas); this.context = gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); // Load extensions gl.getExtension('OES_texture_float'); gl.getExtension('OES_texture_float_linear'); // Init events $(window).on('resize', function() { if (that.$el.innerWidth() != that.canvas.width || that.$el.innerHeight() != that.canvas.height) { canvas.width = that.$el.innerWidth(); canvas.height = that.$el.innerHeight(); } }); this.$el.on('mousemove.ripples', function(e) { if (that.visible && that.running && that.interactive) that.dropAtMouse(e, that.dropRadius, 0.01); }).on('mousedown.ripples', function(e) { if (that.visible && that.running && that.interactive) that.dropAtMouse(e, that.dropRadius * 1.5, 0.14); }); this.textures = []; this.framebuffers = []; for (var i = 0; i < 2; i++) { var texture = gl.createTexture(); var framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); framebuffer.width = this.resolution; framebuffer.height = this.resolution; gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.resolution, this.resolution, 0, gl.RGBA, gl.FLOAT, null); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) { throw new Error('Rendering to this texture is not supported (incomplete framebuffer)'); } gl.bindTexture(gl.TEXTURE_2D, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); this.textures.push(texture); this.framebuffers.push(framebuffer); } this.running = true; // Init GL stuff this.quad = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.quad); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1, -1, +1, -1, +1, +1, -1, +1 ]), gl.STATIC_DRAW); this.initShaders(); // Init textures var image = new Image; image.crossOrigin = ''; image.onload = function() { gl = that.context; function isPowerOfTwo(x) { return (x & (x - 1)) == 0; } var wrapping = (isPowerOfTwo(image.width) && isPowerOfTwo(image.height)) ? gl.REPEAT : gl.CLAMP_TO_EDGE; that.backgroundWidth = image.width; that.backgroundHeight = image.height; var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapping); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapping); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); that.backgroundTexture = texture; // Everything loaded successfully - hide the CSS background image that.$el.css('backgroundImage', 'none'); }; image.src = backgroundUrl; this.visible = true; // Init animation function step() { that.step(); requestAnimationFrame(step); } requestAnimationFrame(step); }; Ripples.DEFAULTS = { resolution: 256, dropRadius: 20, perturbance: 0.03, interactive: true };
以上就是Ripples的构造函数,我们看到这个构造函数非常的冗长,而且其中一些函数还是调用的Ripples原型对象的api,所以我们赶快再把Ripples的原型对象也一起贴出,以下是鲫鱼对Ripples原型对象的定义,我们来看代码。
Ripples.prototype = { step: function() { gl = this.context; if (!this.visible || !this.backgroundTexture) return; this.computeTextureBoundaries(); if (this.running) { this.update(); } this.render(); }, drawQuad: function() { gl.bindBuffer(gl.ARRAY_BUFFER, this.quad); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); }, render: function() { gl.viewport(0, 0, this.canvas.width, this.canvas.height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.useProgram(this.renderProgram.id); bindTexture(this.backgroundTexture, 0); bindTexture(this.textures[0], 1); gl.uniform2fv(this.renderProgram.locations.topLeft, this.renderProgram.uniforms.topLeft); gl.uniform2fv(this.renderProgram.locations.bottomRight, this.renderProgram.uniforms.bottomRight); gl.uniform2fv(this.renderProgram.locations.containerRatio, this.renderProgram.uniforms.containerRatio); gl.uniform1i(this.renderProgram.locations.samplerBackground, 0); gl.uniform1i(this.renderProgram.locations.samplerRipples, 1); this.drawQuad(); }, update: function() { gl.viewport(0, 0, this.resolution, this.resolution); for (var i = 0; i < 2; i++) { gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[i]); bindTexture(this.textures[1-i]); gl.useProgram(this.updateProgram[i].id); this.drawQuad(); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); }, computeTextureBoundaries: function() { var backgroundSize = this.$el.css('background-size'); var backgroundAttachment = this.$el.css('background-attachment'); var backgroundPosition = this.$el.css('background-position').split(' '); // Here the 'window' is the element which the background adapts to // (either the chrome window or some element, depending on attachment) var parElement = backgroundAttachment == 'fixed' ? $window : this.$el; var winOffset = parElement.offset() || {left: pageXOffset, top: pageYOffset}; var winWidth = parElement.innerWidth(); var winHeight = parElement.innerHeight(); // TODO: background-clip if (backgroundSize == 'cover') { var scale = Math.max(winWidth / this.backgroundWidth, winHeight / this.backgroundHeight); var backgroundWidth = this.backgroundWidth * scale; var backgroundHeight = this.backgroundHeight * scale; } else if (backgroundSize == 'contain') { var scale = Math.min(winWidth / this.backgroundWidth, winHeight / this.backgroundHeight); var backgroundWidth = this.backgroundWidth * scale; var backgroundHeight = this.backgroundHeight * scale; } else { backgroundSize = backgroundSize.split(' '); var backgroundWidth = backgroundSize[0] || ''; var backgroundHeight = backgroundSize[1] || backgroundWidth; if (isPercentage(backgroundWidth)) backgroundWidth = winWidth * parseFloat(backgroundWidth) / 100; else if (backgroundWidth != 'auto') backgroundWidth = parseFloat(backgroundWidth); if (isPercentage(backgroundHeight)) backgroundHeight = winHeight * parseFloat(backgroundHeight) / 100; else if (backgroundHeight != 'auto') backgroundHeight = parseFloat(backgroundHeight); if (backgroundWidth == 'auto' && backgroundHeight == 'auto') { backgroundWidth = this.backgroundWidth; backgroundHeight = this.backgroundHeight; } else { if (backgroundWidth == 'auto') backgroundWidth = this.backgroundWidth * (backgroundHeight / this.backgroundHeight); if (backgroundHeight == 'auto') backgroundHeight = this.backgroundHeight * (backgroundWidth / this.backgroundWidth); } } // Compute backgroundX and backgroundY in page coordinates var backgroundX = backgroundPosition[0] || ''; var backgroundY = backgroundPosition[1] || backgroundX; if (backgroundX == 'left') backgroundX = winOffset.left; else if (backgroundX == 'center') backgroundX = winOffset.left + winWidth / 2 - backgroundWidth / 2; else if (backgroundX == 'right') backgroundX = winOffset.left + winWidth - backgroundWidth; else if (isPercentage(backgroundX)) { backgroundX = winOffset.left + (winWidth - backgroundWidth) * parseFloat(backgroundX) / 100; } else { backgroundX = parseFloat(backgroundX); } if (backgroundY == 'top') backgroundY = winOffset.top; else if (backgroundY == 'center') backgroundY = winOffset.top + winHeight / 2 - backgroundHeight / 2; else if (backgroundY == 'bottom') backgroundY = winOffset.top + winHeight - backgroundHeight; else if (isPercentage(backgroundY)) { backgroundY = winOffset.top + (winHeight - backgroundHeight) * parseFloat(backgroundY) / 100; } else { backgroundY = parseFloat(backgroundY); } var elementOffset = this.$el.offset(); this.renderProgram.uniforms.topLeft = new Float32Array([ (elementOffset.left - backgroundX) / backgroundWidth, (elementOffset.top - backgroundY) / backgroundHeight ]); this.renderProgram.uniforms.bottomRight = new Float32Array([ this.renderProgram.uniforms.topLeft[0] + this.$el.innerWidth() / backgroundWidth, this.renderProgram.uniforms.topLeft[1] + this.$el.innerHeight() / backgroundHeight ]); var maxSide = Math.max(this.canvas.width, this.canvas.height); this.renderProgram.uniforms.containerRatio = new Float32Array([ this.canvas.width / maxSide, this.canvas.height / maxSide ]); }, initShaders: function() { var vertexShader = [ 'attribute vec2 vertex;', 'varying vec2 coord;', 'void main() {', 'coord = vertex * 0.5 + 0.5;', 'gl_Position = vec4(vertex, 0.0, 1.0);', '}' ].join('\n'); this.dropProgram = createProgram(vertexShader, [ 'precision highp float;', 'const float PI = 3.141592653589793;', 'uniform sampler2D texture;', 'uniform vec2 center;', 'uniform float radius;', 'uniform float strength;', 'varying vec2 coord;', 'void main() {', 'vec4 info = texture2D(texture, coord);', 'float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - coord) / radius);', 'drop = 0.5 - cos(drop * PI) * 0.5;', 'info.r += drop * strength;', 'gl_FragColor = info;', '}' ].join('\n')); this.updateProgram = [0,0]; this.updateProgram[0] = createProgram(vertexShader, [ 'precision highp float;', 'uniform sampler2D texture;', 'uniform vec2 delta;', 'varying vec2 coord;', 'void main() {', 'vec4 info = texture2D(texture, coord);', 'vec2 dx = vec2(delta.x, 0.0);', 'vec2 dy = vec2(0.0, delta.y);', 'float average = (', 'texture2D(texture, coord - dx).r +', 'texture2D(texture, coord - dy).r +', 'texture2D(texture, coord + dx).r +', 'texture2D(texture, coord + dy).r', ') * 0.25;', 'info.g += (average - info.r) * 2.0;', 'info.g *= 0.995;', 'info.r += info.g;', 'gl_FragColor = info;', '}' ].join('\n')); gl.uniform2fv(this.updateProgram[0].locations.delta, this.textureDelta); this.updateProgram[1] = createProgram(vertexShader, [ 'precision highp float;', 'uniform sampler2D texture;', 'uniform vec2 delta;', 'varying vec2 coord;', 'void main() {', 'vec4 info = texture2D(texture, coord);', 'vec3 dx = vec3(delta.x, texture2D(texture, vec2(coord.x + delta.x, coord.y)).r - info.r, 0.0);', 'vec3 dy = vec3(0.0, texture2D(texture, vec2(coord.x, coord.y + delta.y)).r - info.r, delta.y);', 'info.ba = normalize(cross(dy, dx)).xz;', 'gl_FragColor = info;', '}' ].join('\n')); gl.uniform2fv(this.updateProgram[1].locations.delta, this.textureDelta); this.renderProgram = createProgram([ 'precision highp float;', 'attribute vec2 vertex;', 'uniform vec2 topLeft;', 'uniform vec2 bottomRight;', 'uniform vec2 containerRatio;', 'varying vec2 ripplesCoord;', 'varying vec2 backgroundCoord;', 'void main() {', 'backgroundCoord = mix(topLeft, bottomRight, vertex * 0.5 + 0.5);', 'backgroundCoord.y = 1.0 - backgroundCoord.y;', 'ripplesCoord = vec2(vertex.x, -vertex.y) * containerRatio * 0.5 + 0.5;', 'gl_Position = vec4(vertex.x, -vertex.y, 0.0, 1.0);', '}' ].join('\n'), [ 'precision highp float;', 'uniform sampler2D samplerBackground;', 'uniform sampler2D samplerRipples;', 'uniform float perturbance;', 'varying vec2 ripplesCoord;', 'varying vec2 backgroundCoord;', 'void main() {', 'vec2 offset = -texture2D(samplerRipples, ripplesCoord).ba;', 'float specular = pow(max(0.0, dot(offset, normalize(vec2(-0.6, 1.0)))), 4.0);', 'gl_FragColor = texture2D(samplerBackground, backgroundCoord + offset * perturbance) + specular;', '}' ].join('\n')); gl.uniform1f(this.renderProgram.locations.perturbance, this.perturbance); }, dropAtMouse: function(e, radius, strength) { this.drop( e.pageX - this.$el.offset().left, e.pageY - this.$el.offset().top, radius, strength ); }, drop: function(x, y, radius, strength) { var that = this; gl = this.context; var elWidth = this.$el.outerWidth(); var elHeight = this.$el.outerHeight(); var longestSide = Math.max(elWidth, elHeight); radius = radius / longestSide; var dropPosition = new Float32Array([ (2 * x - elWidth) / longestSide, (elHeight - 2 * y) / longestSide ]); gl.viewport(0, 0, this.resolution, this.resolution); // Render onto texture/framebuffer 0 gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[0]); // Using texture 1 bindTexture(this.textures[1]); gl.useProgram(this.dropProgram.id); gl.uniform2fv(this.dropProgram.locations.center, dropPosition); gl.uniform1f(this.dropProgram.locations.radius, radius); gl.uniform1f(this.dropProgram.locations.strength, strength); this.drawQuad(); // Switch textures var t = this.framebuffers[0]; this.framebuffers[0] = this.framebuffers[1]; this.framebuffers[1] = t; t = this.textures[0]; this.textures[0] = this.textures[1]; this.textures[1] = t; gl.bindFramebuffer(gl.FRAMEBUFFER, null); }, // Actions destroy: function() { this.canvas.remove(); this.$el.off('.ripples'); this.$el.css('backgroundImage', ''); this.$el.removeClass('jquery-ripples').removeData('ripples'); }, show: function() { this.$canvas.show(); this.$el.css('backgroundImage', 'none'); this.visible = true; }, hide: function() { this.$canvas.hide(); this.$el.css('backgroundImage', ''); this.visible = false; }, pause: function() { this.running = false; }, play: function() { this.running = true; }, set: function(property, value) { switch (property) { case 'interactive': this.interactive = value; break; } } };
这就是Ripple的原型,我们一个一个函数来过,第一个函数step
step: function() { gl = this.context; if (!this.visible || !this.backgroundTexture) return; this.computeTextureBoundaries(); if (this.running) { this.update(); } this.render(); },
这是下一帧的渲染函数,(1)创建gl对象,(2)判断视图可见,背景纹理可见,不可见即返回,(3)计算纹理边界,(4)如果this.running,就更新视图,(5)渲染,先这样解释,读者接着看第二个函数。
drawQuad: function() { gl.bindBuffer(gl.ARRAY_BUFFER, this.quad); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); },
这是绘制三角扇图元。(1)传入顶点this.quad,(2)绑定vertex-shader顶点着色器,底0个参数,数组节点跨度2,浮点类型,(3)使用drawArrays绘制四边形,从第0个点开始绘制,4个点一个图元四边形。我们通过drawQuad就能画四边形,没什么难的,我们继续看下一个函数。
render: function() { gl.viewport(0, 0, this.canvas.width, this.canvas.height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.useProgram(this.renderProgram.id); bindTexture(this.backgroundTexture, 0); bindTexture(this.textures[0], 1); gl.uniform2fv(this.renderProgram.locations.topLeft, this.renderProgram.uniforms.topLeft); gl.uniform2fv(this.renderProgram.locations.bottomRight, this.renderProgram.uniforms.bottomRight); gl.uniform2fv(this.renderProgram.locations.containerRatio, this.renderProgram.uniforms.containerRatio); gl.uniform1i(this.renderProgram.locations.samplerBackground, 0); gl.uniform1i(this.renderProgram.locations.samplerRipples, 1); this.drawQuad(); },
这是渲染,我们遵循的就是向renderProgram着色器中传uniform参数。我们一起来看看下一个向着色器传参的函数。
update: function() { gl.viewport(0, 0, this.resolution, this.resolution); for (var i = 0; i < 2; i++) { gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[i]); bindTexture(this.textures[1-i]); gl.useProgram(this.updateProgram[i].id); this.drawQuad(); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); },
这就是刷新,遵循向updateProgram着色器中传uniform参数。看到这里,我们马上把着色器函数都扒出来看一下。
initShaders: function() { var vertexShader = [ 'attribute vec2 vertex;', 'varying vec2 coord;', 'void main() {', 'coord = vertex * 0.5 + 0.5;', 'gl_Position = vec4(vertex, 0.0, 1.0);', '}' ].join('\n'); this.dropProgram = createProgram(vertexShader, [ 'precision highp float;', 'const float PI = 3.141592653589793;', 'uniform sampler2D texture;', 'uniform vec2 center;', 'uniform float radius;', 'uniform float strength;', 'varying vec2 coord;', 'void main() {', 'vec4 info = texture2D(texture, coord);', 'float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - coord) / radius);', 'drop = 0.5 - cos(drop * PI) * 0.5;', 'info.r += drop * strength;', 'gl_FragColor = info;', '}' ].join('\n')); this.updateProgram = [0,0]; this.updateProgram[0] = createProgram(vertexShader, [ 'precision highp float;', 'uniform sampler2D texture;', 'uniform vec2 delta;', 'varying vec2 coord;', 'void main() {', 'vec4 info = texture2D(texture, coord);', 'vec2 dx = vec2(delta.x, 0.0);', 'vec2 dy = vec2(0.0, delta.y);', 'float average = (', 'texture2D(texture, coord - dx).r +', 'texture2D(texture, coord - dy).r +', 'texture2D(texture, coord + dx).r +', 'texture2D(texture, coord + dy).r', ') * 0.25;', 'info.g += (average - info.r) * 2.0;', 'info.g *= 0.995;', 'info.r += info.g;', 'gl_FragColor = info;', '}' ].join('\n')); gl.uniform2fv(this.updateProgram[0].locations.delta, this.textureDelta); this.updateProgram[1] = createProgram(vertexShader, [ 'precision highp float;', 'uniform sampler2D texture;', 'uniform vec2 delta;', 'varying vec2 coord;', 'void main() {', 'vec4 info = texture2D(texture, coord);', 'vec3 dx = vec3(delta.x, texture2D(texture, vec2(coord.x + delta.x, coord.y)).r - info.r, 0.0);', 'vec3 dy = vec3(0.0, texture2D(texture, vec2(coord.x, coord.y + delta.y)).r - info.r, delta.y);', 'info.ba = normalize(cross(dy, dx)).xz;', 'gl_FragColor = info;', '}' ].join('\n')); gl.uniform2fv(this.updateProgram[1].locations.delta, this.textureDelta); this.renderProgram = createProgram([ 'precision highp float;', 'attribute vec2 vertex;', 'uniform vec2 topLeft;', 'uniform vec2 bottomRight;', 'uniform vec2 containerRatio;', 'varying vec2 ripplesCoord;', 'varying vec2 backgroundCoord;', 'void main() {', 'backgroundCoord = mix(topLeft, bottomRight, vertex * 0.5 + 0.5);', 'backgroundCoord.y = 1.0 - backgroundCoord.y;', 'ripplesCoord = vec2(vertex.x, -vertex.y) * containerRatio * 0.5 + 0.5;', 'gl_Position = vec4(vertex.x, -vertex.y, 0.0, 1.0);', '}' ].join('\n'), [ 'precision highp float;', 'uniform sampler2D samplerBackground;', 'uniform sampler2D samplerRipples;', 'uniform float perturbance;', 'varying vec2 ripplesCoord;', 'varying vec2 backgroundCoord;', 'void main() {', 'vec2 offset = -texture2D(samplerRipples, ripplesCoord).ba;', 'float specular = pow(max(0.0, dot(offset, normalize(vec2(-0.6, 1.0)))), 4.0);', 'gl_FragColor = texture2D(samplerBackground, backgroundCoord + offset * perturbance) + specular;', '}' ].join('\n')); gl.uniform1f(this.renderProgram.locations.perturbance, this.perturbance); },
以上是着色器代码,我们看到shader中是通过修改texture纹理的uv坐标位置每次update纹理的最新位置,这就是水波扩散的原理。每次获取鼠标移动的最新位置,以此为圆心,固定半径确定uv的最新坐标,然后重新按变形的坐标贴图,就出现了水波扩散的特效。
以上就是水波特效的插件代码,有点复杂,纯属texture材质特效,希望读者能不吝斧正。今天先到这里,鲫鱼谢谢大家阅读。祝读者们周末愉快。
本文系原作,如需引用,请注明出处:https://www.cnblogs.com/ccentry/p/10087065.html