[WebGL] 百万流畅流体粒子

4,769 阅读5分钟

前言

前段时间领导要我做一个流体粒子的demo,正巧我也最近在看一些流体、烟雾的文章,接到任务兴奋劲儿一下就上来了,因为没有实践过,而且效果看着很不错,于是乎我脑海里就在想整个流程,遇到其中一个硬性问题就是流体粒子每一个都有自己的速度和坐标,那我用js去运算且更新这些数据岂不是很耗性能,就在上次出差参加一个峰会中,(头脑出差)一直在想怎样解决,突然想到可以利用GPU生成图片数据,然后解析数据传递给相应粒子上不就得了,做出来后性能没得说,我这个激动啊~。。

demo地址(线上我只上传了6w的粒子)

性能对比

接下来的两张性能对比图是基于cpu 6倍降速所截的图片,第一张图流体粒子采用js运算粒子位置(用js运算每个位置的话,粒子总数应该不多,否则会卡顿),性能分析时可以看到每一帧得黄色(js运算)区域占比很大,帧率不齐,第二张图是我采用得方式,由于运算转移到GPU中,每一帧得黄色区域占比很小,100w粒子的情况下依然帧率很齐。

思路

实现思路中主要用到frameBuffer来制作数据图片,数据图片中a区域每个像素代表当前粒子的位置,b则代表当前的速度。因为要加入手指滑动,所以我做了两个矢量场,c代表图形噪声矢量场,d代表手指滑动的矢量场。

在每一次绘制数据图片时,b(速度)区域是结合两个矢量场(c&d)以及他自身的速度运算生成的,a(位置)区域只需要本身坐标结合速度生成新坐标,c是根据时间进行的噪声图片,d则需要根据传入的touch坐标与速度进行计算。

生成完数据图片之后,才是开始进行更新粒子位置的环节,其思路是根据传入的粒子索引查找数据图片中相应a区域的rgb值,这个rgb值就是当前索引粒子的位置,有了位置之后,接下来就是常规绘制点的步骤。

梳理到这儿,重新回看整个流程,js逻辑几乎很少,只有创建粒子索引,每一帧传入c区域所需要的时间,以及滑动时需要传入touch的位置与速度向量,其他的全部在GPU中完成,所以在性能测试中几乎看不到黄色(js)区域,并且性能稳定高效。

数据图片实现

js部分

  1. 创建FBO并为FBO绑定texture
    const frameBuffer = this.gl.createFramebuffer();
    const texture = this.gl.createTexture();
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER,frameBuffer);
    this.gl.activeTexture(this.gl.TEXTURE0);
    this.gl.bindTexture(this.gl.TEXTURE_2D,texture);
    this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGB,size,size,0,this.gl.RGB,this.halfFloat.HALF_FLOAT_OES,null);
    this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MAG_FILTER,this.gl.NEAREST);
    this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.NEAREST);
    this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE);
    this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE);
    this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER,this.gl.COLOR_ATTACHMENT0,this.gl.TEXTURE_2D,texture,0);
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER,null);
  1. 在每一帧绘制时进行绑定FBO
    this.gl.useProgram(this.vectorData.program);
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER,frameBuffer);
    this.gl.clear(this.gl.COLOR_BUFFER_BIT);
    ...
    this.gl.drawArrays(this.gl.TRIANGLE_STRIP,0,4);
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER,null);
  1. 数据绘制完成后作为texture传给粒子Program
    this.gl.useProgram(this.particleData.program);
    this.gl.activeTexture(this.gl.TEXTURE0);
    //传入数据图片
    this.gl.uniform1i(this.gl.getUniformLocation(this.particleData.program,'uTexture'),0);
    this.gl.bindTexture(this.gl.TEXTURE_2D,this.vectorTexture);
    //传入粒子索引数据
    this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.particleData.buffer);
    this.gl.vertexAttribPointer(this.particleData.aPosition,2,this.gl.FLOAT,false,0,0);
    this.gl.drawArrays(this.gl.POINTS,0,this.particleLength);

shader部分 数据图片主要使用到了片元着色器去设置每个像素的颜色

#ifdef GL_ES
    precision highp float;
#endif
#define PI 3.14159265358979323846
#define OCTAVE_NUM 5
#define SIZE 128.
uniform sampler2D uTexture;//上次的数据图片
uniform float uTime;//时间
uniform vec4 uPointer;//手指坐标与速度向量
varying vec2 vPosition;//从顶点着色器传入的位置
const float pixelSize = 1./SIZE;
vec2 random (in vec2 p) {
    //伪随机函数
    return  fract(
        sin(
            vec2(
                dot(p, vec2(3.3,6.1)),
                dot(p, vec2(5.7,4.7))
            )
        ) * .5
    );
}
float noise(in vec2 p){
    //噪声函数
    vec2 i = floor(p);
    vec2 f = fract(p);
    float a = dot(random(i),f);
    float b = dot(random(i + vec2(1., 0.)),f - vec2(1., 0.));
    float c = dot(random(i + vec2(0., 1.)),f - vec2(0., 1.));
    float d = dot(random(i + vec2(1., 1.)),f - vec2(1., 1.));
    vec2 u = smoothstep(0.,1.,f);
    return mix(mix(a,b,u.x),mix(c,d,u.x),u.y)+.5;
}
vec4 randomRate(in vec3 pos){
    //计算c区域 vector field 速度
    vec3 _pos = pos*vec3(.5,.5,1);
    vec3 pixelPos = _pos*SIZE;
    _pos.y += .5;
    vec3 i = floor(pixelPos);
    vec3 f = fract(pixelPos);
    //偏移特征的点选择左下角 因为右下角在 ==1 时 有问题
    vec3 a = texture2D(uTexture,_pos.xy).xyz;
    vec3 b = texture2D(uTexture,_pos.xy+vec2(-pixelSize,0)).xyz;
    vec3 c = texture2D(uTexture,_pos.xy+vec2(0,pixelSize)).xyz;
    vec3 d = texture2D(uTexture,_pos.xy+vec2(-pixelSize,pixelSize)).xyz;
    vec3 u = smoothstep(0.,1.,f);
    vec3 _mix = mix(mix(a,b,u.x),mix(c,d,u.x),u.y);
    _mix-=.5;
    return vec4(_mix,1);
}
vec4 touchRate(in vec3 pos){
    //计算d区域 vector field 速度
    vec3 _pos = pos*vec3(.5,.5,1);
    vec3 pixelPos = _pos*SIZE;
    _pos.y += .5;
    _pos.x += .5;
    vec3 i = floor(pixelPos);
    vec3 f = fract(pixelPos);
    vec3 a = texture2D(uTexture,_pos.xy).xyz;
    vec3 b = texture2D(uTexture,_pos.xy+vec2(pixelSize,0)).xyz;
    vec3 c = texture2D(uTexture,_pos.xy+vec2(0,pixelSize)).xyz;
    vec3 d = texture2D(uTexture,_pos.xy+vec2(pixelSize)).xyz;
    vec3 u = smoothstep(0.,1.,f);
    vec3 _mix = mix(mix(a,b,u.x),mix(c,d,u.x),u.y);
    _mix-=.5;
    return vec4(_mix,1);
}
void main(){
    vec4 color = texture2D(uTexture,vPosition);
    if(vPosition.y<.5){
        if(vPosition.x<.5){
            //a区域的计算 xyz = rgb
            vec4 v = texture2D(uTexture,vPosition+vec2(.5,0))-.5;
            v/=pow(2.,12.);
            v.xy *= (1.-cos(color.z*PI*1.5))/3.;
            if(color.z+v.z<=.0){
                //重置位置  解决一段时间后 粒子重合得问题
                gl_FragColor = vec4(vPosition*2.,1,1);
            }else{
                gl_FragColor = fract(color+v);
            }
        }else{
            //b区域计算
            //当前粒子坐标获取
            vec3 currentPos = texture2D(uTexture,vPosition+vec2(-.5,0)).xyz;
            gl_FragColor=color+(randomRate(currentPos)+touchRate(currentPos))*512.;
            gl_FragColor = .5+(gl_FragColor-.5)/5.;
        }
    }else{
        vec2 offset = vec2(sin(uTime/27.)*sin(uTime),cos(uTime/17.)*cos(uTime)*.5);
        gl_FragColor=vec4(0.5,0.5,0.5,1);
        if(vPosition.x<.5){
            //c区域计算
            gl_FragColor.rg=vec2(noise(vPosition*4.+offset*2.3),noise(vPosition*3.+offset.yx));
            gl_FragColor.b = .4;
        }else{
            //d区域计算
            vec2 _point = uPointer.xy/2.+vec2(.5);
            float _len = distance(_point,vPosition);
            vec2 _color = (color.xy-vec2(.5))*.99;
            if(length(_color)<.04){
                _color = vec2(0);
            }
            gl_FragColor.rg = _color;
            if(uPointer.x>=.0&&_len<.05){
                gl_FragColor.rg+=(1.-_len/.05)*uPointer.zw;
            }
            gl_FragColor.rg += vec2(.5);
        }
    }
    gl_FragColor.a = 1.;
}

感觉代码好多~。。看不懂的话 我再分细点。。,其实主要是讲的思路,利用texture可以把逻辑放入GPU中实现流畅特效。