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
WebGL——水波纹特效的更多相关文章
- Android水波纹特效的简单实现
我的开源页面指示器框架 MagicIndicator,各位一定不要错过哦. 水波纹特效,想必大家或多或少见过,在我的印象中,大致有如下几种: 支付宝 "咻咻咻" 式 流量球 &qu ...
- 2.CCGridAction(3D效果),3D反转特效,凸透镜特效,液体特效,3D翻页特效,水波纹特效,3D晃动的特效,扭曲旋转特效,波动特效,3D波动特效
1 类图组织 2 实例 CCSprite * spr = CCSprite::create("HelloWorld.png"); spr->setPosition(cc ...
- CSS 按钮水波纹特效
/* 按钮反馈之波纹 */ .ripple { position: relative; /* overflow:hidden */ 打开注释及效果不扩散在外 } .ripple:focus{ out ...
- Android5.0以上的项目都会有的按钮点击特效--水波纹
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http: ...
- android 5.0 水波纹 实现
1. 定义一个普通圆角背景的xml; rounded_corners.xml <?xml version="1.0" encoding="utf-8"?& ...
- canvas实现水波纹效果
本文将会从水波的基本原理开始,详细讲解在canvas中模拟水波扩散,分析并计算水波的能量分布,并通过振幅模拟水波对图像的折射效果,最后实现水波特效. 水波基本原理 首先复习一波高中物理知识. 波是指振 ...
- HTML5 Canvas水波纹动画特效
HTML5的Canvas特性非常实用,我们不仅可以在Canvas画布上绘制各种图形,也可以制作绚丽的动画,比如这次介绍的水波纹动画特效.以前我们也分享过一款基于HTML5 WebGL的水波荡漾动画,让 ...
- Android特效专辑(十一)——仿水波纹流量球进度条控制器,实现高端大气的主流特效
Android特效专辑(十一)--仿水波纹流球进度条控制器,实现高端大气的主流特效 今天看到一个效果挺不错的,就模仿了下来,加上了一些自己想要的效果,感觉还不错的样子,所以就分享出来了,话不多说,上图 ...
- Android特效专辑(十)——点击水波纹效果实现,逻辑清晰实现简单
Android特效专辑(十)--点击水波纹效果实现,逻辑清晰实现简单 这次做的东西呢,和上篇有点类似,就是用比较简单的逻辑思路去实现一些比较好玩的特效,最近也是比较忙,所以博客更新的速度还得看时间去推 ...
随机推荐
- css画图那些事
上一篇css3写了一些基本的图形,想到是不是能用css3画个动物,便在网上找图片.于是选中一只大鹏鸟 也不难,一步步的写出身体部位,再定位上去就好了.来一张效果图,后面给两个加了动画,稍微难看一点,后 ...
- 【笔记】关于TCP三次握手和四次挥手的理解
1. 三次握手: 服务器一定处于Listen状态,否则客户端发过来的连接会被拒绝.注:服务器和客户端的角色是相对的. 客户端发送第一次握手(客户端发送连接请求(SYNC包)到服务器)之后由Closed ...
- 新手学Linux:在VMware14中安装CentOS7详细教程
VMware Workstation14安装CentOS7.0 详情教程 1.准备工作 a)下载VMware workstation14 b)下载CentOS7 c)下载SSH Secure Shel ...
- mysql数据类型和使用方法
版权声明:本文为博主原创文章.未经博主同意不得转载. https://blog.csdn.net/u011729265/article/details/25086957 MySQL支持 ...
- 批量删除Redis中的数据
测试环境上是docker安装的redis,生产上使用的是阿里云Redis服务,需要批量清理生产上的数据. 阿里云提供了BS结构的工具管理Redis,但是不能全选批量删除,只能脚本删除,方法是在测试环境 ...
- 1305. [CQOI2009]跳舞【最大流+二分】
Description 一次舞会有n个男孩和n个女孩.每首曲子开始时,所有男孩和女孩恰好配成n对跳交谊舞.每个男孩都不会和同一个女孩跳两首(或更多)舞曲.有一些男孩女孩相互喜欢,而其他相互不喜欢(不会 ...
- ethereumjs/ethereumjs-common-1-简介
为了了解ethereumjs/ethereumjs-block-3-代码的使用需要了解的一个模块 https://github.com/ethereumjs/ethereumjs-common Com ...
- Kafka设计解析(十)Kafka如何创建topic
转载自 huxihx,原文链接 Kafka如何创建topic? 目录 一.命令行部分 二.后台逻辑部分 Kafka创建topic命令很简单,一条命令足矣: bin/kafka-topics. --re ...
- 包学会之浅入浅出Vue.js:结业篇
在第一篇<包学会之浅入浅出Vue.js:开学篇>和上一篇<包学会之浅入浅出Vue.js:升学篇>的学习中,我们首先了解了Vue环境的搭建以及两个重要思想——路由和组件的学习,通 ...
- UCOS时钟节拍的讲究
其实这个值取适中即可,100,200都行,看你的片子是什么,Cortex-M3的片子取200较合适这个值太小,系统调度周期较长,各个任务之间切换较慢,适时性降低,而太大了,中断周期与调试周期接近了,那 ...