zoukankan      html  css  js  c++  java
  • 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('
    ');
                
                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('
    '));
                
                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('
    '));
                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('
    '));
                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('
    '), [
                    '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('
    '));
                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('
    ');
                
                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('
    '));
                
                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('
    '));
                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('
    '));
                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('
    '), [
                    '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('
    '));
                gl.uniform1f(this.renderProgram.locations.perturbance, this.perturbance);
            },

    以上是着色器代码,我们看到shader中是通过修改texture纹理的uv坐标位置每次update纹理的最新位置,这就是水波扩散的原理。每次获取鼠标移动的最新位置,以此为圆心,固定半径确定uv的最新坐标,然后重新按变形的坐标贴图,就出现了水波扩散的特效。
    以上就是水波特效的插件代码,有点复杂,纯属texture材质特效,希望读者能不吝斧正。今天先到这里,鲫鱼谢谢大家阅读。祝读者们周末愉快。

    本文系原作,如需引用,请注明出处:https://www.cnblogs.com/ccentry/p/10087065.html                       

  • 相关阅读:
    程序员副业那些事:聊聊出书和录视频
    跳槽时,不敢要高工资也会对候选人不利
    SQL 查询今天、昨天、7天内、30天的数据
    jquery table按列名称排序
    Asp.Net微信js分享
    表格插件BootStrap-Table使用教程
    ASP.NET中IOC容器Autofac(依赖注入DI 控制反转IOC)
    IIS添加MIME类型.woff/.svg/.woff2/.eot/.otf.ttf
    div垂直居中水平居中css
    Asp.Net报https请求报传输流收到意外的 EOF 或 0 个字节
  • 原文地址:https://www.cnblogs.com/ccentry/p/10087065.html
Copyright © 2011-2022 走看看