[WebGL] 百萬流暢流體粒子

前言

前段時間領導要我作一個流體粒子的demo,正巧我也最近在看一些流體、煙霧的文章,接到任務興奮勁兒一下就上來了,由於沒有實踐過,並且效果看着很不錯,因而乎我腦海裏就在想整個流程,遇到其中一個硬性問題就是流體粒子每個都有本身的速度和座標,那我用js去運算且更新這些數據豈不是很耗性能,就在上次出差參加一個峯會中,(頭腦出差)一直在想怎樣解決,忽然想到能夠利用GPU生成圖片數據,而後解析數據傳遞給相應粒子上不就得了,作出來後性能沒得說,我這個激動啊~。。html

demo地址(線上我只上傳了6w的粒子)web

性能對比

接下來的兩張性能對比圖是基於cpu 6倍降速所截的圖片,第一張圖流體粒子採用js運算粒子位置(用js運算每一個位置的話,粒子總數應該很少,不然會卡頓),性能分析時能夠看到每一幀得黃色(js運算)區域佔比很大,幀率不齊,第二張圖是我採用得方式,因爲運算轉移到GPU中,每一幀得黃色區域佔比很小,100w粒子的狀況下依然幀率很齊。typescript

思路

實現思路中主要用到frameBuffer來製做數據圖片,數據圖片中a區域每一個像素表明當前粒子的位置,b則表明當前的速度。由於要加入手指滑動,因此我作了兩個矢量場,c表明 圖形噪聲矢量場,d表明手指滑動的矢量場。

在每一次繪製數據圖片時,b(速度)區域是結合兩個矢量場(c&d)以及他自身的速度運算生成的,a(位置)區域只須要自己座標結合速度生成新座標,c是根據時間進行的噪聲圖片,d則須要根據傳入的touch座標與速度進行計算。app

生成完數據圖片以後,纔是開始進行更新粒子位置的環節,其思路是根據傳入的粒子索引查找數據圖片中相應a區域的rgb值,這個rgb值就是當前索引粒子的位置,有了位置以後,接下來就是常規繪製點的步驟。dom

梳理到這兒,從新回看整個流程,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中實現流暢特效。ui

相關文章
相關標籤/搜索