你們好,今天我ccentry要作一個水波紋特效,咱們來看看水波紋特效的作法。首先咱們來看一下水波紋特效的效果是怎麼樣的,請看下圖。css
咱們要作的就是相似這種紋理特效,那麼咱們來看看是如何製做的吧。首先鯽魚我新建一個空項目,來編寫這個demo,項目結構以下圖所示。html
img文件夾中存放的是uv貼圖和底圖,js文件夾下存放的是jquery和個人水波紋效果的js文件,還有就是展現頁面index.html。很簡單,沒什麼東西了,接下來就來看鯽魚我是怎麼實現上面這個水波紋特效的吧。咱們先開始編輯ripples.js,這就是這個特效的核心功能。再來看一遍工程結構。jquery
且看鯽魚我怎麼寫這個ripple.js。首先咱們檢查瀏覽器是否支持webgl的api,這是一個瀏覽器校驗功能函數,代碼塊以下。web
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'對象,鯽魚相信你們也都看得懂,咱們就不在這裏浪費時間了,繼續往下看。chrome
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,這裏再也不贅述。咱們再往下看。canvas
function bindTexture(texture, unit) { gl.activeTexture(gl.TEXTURE0 + (unit || 0)); gl.bindTexture(gl.TEXTURE_2D, texture); }
這個函數是綁定材質的函數,參數texture是材質,unit是偏移量,這個函數只有兩步操做,啓用gl.TEXTURE0材質類型,將傳入的材質按照該類型去綁定到gl程序對象上。該函數作到這件事,咱們看下去。api
//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材質特效,但願讀者能不吝斧正。今天先到這裏,鯽魚謝謝你們閱讀。祝讀者們週末愉快。
本文系原做,如需引用,請註明出處:http://www.javashuo.com/article/p-hcyvpouv-ex.html