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特效专辑(十)--点击水波纹效果实现,逻辑清晰实现简单 这次做的东西呢,和上篇有点类似,就是用比较简单的逻辑思路去实现一些比较好玩的特效,最近也是比较忙,所以博客更新的速度还得看时间去推 ...
随机推荐
- java内部类案例
实现键值对的存储输出 import java.util.Arrays; public class EntryDemoTest { //实现键值对的存储 public static void main( ...
- NSProxy应用例子
动态代理模式的应用很多,特别是在不能修改被代理类的前提下,要对执行某些方法时需要打log或者捕捉异常等处理时,是一个非常方便的方法.只需要少量修改客户端(场景类)代码和添加一个代理类就可以实现,这个符 ...
- CPU中MMU的作用
http://blog.csdn.net/jjw97_5/article/details/39340261 MMU是个硬件,每当cpu访问一个地址的时候,MMU从内存里面查table,把cpu想访问的 ...
- 20165318 2017-2018-2 《Java程序设计》第三周学习总结
20165318 2017-2018-2 <Java程序设计>第三周学习总结 学习总结 我感觉从这一章开始,新的知识点扑面而来,很多定义都是之前没有接触过的,看书的时候难免有些晦涩.但由于 ...
- 【洛谷】【堆+贪心】P1484 种树
[题目描述:] cyrcyr今天在种树,他在一条直线上挖了n个坑.这n个坑都可以种树,但为了保证每一棵树都有充足的养料,cyrcyr不会在相邻的两个坑中种树.而且由于cyrcyr的树种不够,他至多会种 ...
- Day6 重载构造
带参数方法 [1]无参数,无返回值 void 方法名(){方法体:} [2]无参数,有返回值 int 方法名(){方法体:} [3]有参数,无返回值 void 方法名(int num){方法体:} [ ...
- 记一次ajax交互问题
问题描述:仍然在表单中,像往常一样,异步获取表单内所有输入框的信息,点击提交,确实也走了后台,但是结果却走了ajax的error.这一点使我当时非常疑惑,为什么会走error呢? 一般走error的原 ...
- python range函数与numpy arange函数,xrange与range的区别
转自:https://www.cnblogs.com/ymjyqsx/p/6426764.html 1.range()返回的是range object,而np.arange()返回的是numpy.nd ...
- PHP的Reflection反射机制
更多内容推荐微信公众号,欢迎关注: 原文地址: http://www.nowamagic.net/php/php_Reflection.php PHP5添加了一项新的功能:Reflection.这个功 ...
- ajax表单提交post(错误400) 序列化表单(post表单转换json(序列化))
序列化表单 使用serializeArray()序列化 转换成json格式 function arrayTOjson(node) { var b = "{"; for (var i ...