Language
cover_photo

ThreeJS 粒子特效筆記 03:FBO 技術

從零開始理解 GPGPU Particle System 的 Compute Shader 思維

Quick answer

ThreeJS 粒子特效筆記 03:FBO 技術:這篇筆記整理 FBO technique,也就是在 WebGL 中用 framebuffer 與 render target 來模擬 compute shader 工作流的方式。

審閱狀態:可審閱後發布

這篇筆記整理 FBO technique,也就是在 WebGL 中用 framebuffer 與 render target 來模擬 compute shader 工作流的方式。

在一般渲染流程中,shader 的結果會被畫到螢幕上。但在 FBO 工作流中,我們把 shader 的計算結果寫進一張 texture,下一次再把這張 texture 當成資料讀回來。

這對粒子系統非常重要。粒子的座標、速度或狀態可以儲存在 texture 裡,並由 GPU 平行更新。CPU 不需要逐顆粒子計算,因此可以處理更大量的資料。

理解 FBO 的關鍵,是把 texture 想成資料表,而不只是圖片。每個 pixel 可以代表一個粒子的狀態,fragment shader 則負責更新這些狀態。

建議發佈前補上 ping-pong render target 的圖解,並確認原始程式碼片段仍符合目前 Three.js 版本。

相關素材

以下整理原文中的圖片、連結、程式碼與 MDX component,作為延伸閱讀與技術參考。

連結

程式碼與設定片段

片段 1

createPositionBuffer(){
    this.size = 32;
    this.number = this.size * this.size;

    this.positions = new Float32Array(this.number * 4);
    for(let i=0; i<this.size; i++){
      for(let j=0; j<this.size; j++){
        let index = i * this.size + j;
        this.positions[index * 4 ] = i/(this.size-1) - 0.5;
        this.positions[index * 4 + 1] = j/(this.size-1) -0.5;
        this.positions[index * 4 + 2] = 0;
        this.positions[index * 4 + 3] = 1;
      }
    }
    this.positionTexture = new THREE.DataTexture(this.positions,this.size, this.size, THREE.RGBAFormat, THREE.FloatType);
    this.positionTexture.needsUpdate = true;

  }

片段 2

setupFBO(){
    this.sceneFBO = new THREE.Scene();
    this.cameraFBO = new THREE.OrthographicCamera(-1,1,1,-1,0, 1);
    this.cameraFBO.position.z = 1;
    this.cameraFBO.lookAt(new THREE.Vector3(0,0,0));

    

    this.geoFBO = new THREE.PlaneGeometry(2,2);
    this.matFBO = new THREE.ShaderMaterial({
      uniforms: {
        uMousePos: {value: this.uMousePos},
        uPosTexture: {value: this.positionTexture},
        uOriginPosTexture: {value: this.positionTexture}
      },
      vertexShader:fboVertex,
      fragmentShader:fboFragment, 
    })
    this.meshFBO = new THREE.Mesh(this.geoFBO, this.matFBO);
    // this.meshFBO.position.x = 0.5;
    this.sceneFBO.add(this.meshFBO);
    ...
    }

片段 3

setupFBO(){
...
/*
       * .magFilter : number (THREE.LinearFilter or THREE.NearestFilter)
       * How the texture is sampled when a texel covers more than one pixel. The default is THREE.LinearFilter, which takes the four closest texels and bilinearly interpolates among them. The other option is THREE.NearestFilter, which uses the value of the closest texel.
       * See the texture constants page for details.

       * .minFilter : number (THREE.LinearFilter or THREE.NearestFilter)
       * How the texture is sampled when a texel covers less than one pixel. The default is THREE.LinearMipmapLinearFilter, which uses mipmapping and a trilinear filter.
       * 
       * type : number() (THREE.UnsignedByteType 
              THREE.ByteType 
              THREE.ShortType
              THREE.UnsignedShortType 
              THREE.IntType 
              THREE.UnsignedIntType
              THREE.FloatType 
              THREE.HalfFloatType 
              THREE.UnsignedShort4444Type
              THREE.UnsignedShort5551Type 
              THREE.UnsignedInt248Type)
       * This must correspond to the .format. The default is THREE.UnsignedByteType, which will be used for most texture formats.
       */
    this.renderTarget = new THREE.WebGLRenderTarget(this.size,this.size,{
      minFilter: THREE.NearestFilter,
      magFilter: THREE.NearestFilter,
      type: THREE.FloatType
      
    })
    }

片段 4

addObject(){
    this.geometry = new THREE.PlaneGeometry(10,10,50,50);
    this.material = new THREE.MeshNormalMaterial();
    
    this.time = 0;
    this.material = new THREE.ShaderMaterial({
        uniforms: {
            time: {value: this.time},
            uTexture: this.positionTexture
        },
        vertexShader: vertexShader,
        fragmentShader: fragmentShader,
    })
    this.mesh = new THREE.Points(this.geometry, this.material);
    this.scene.add(this.mesh);
    }

片段 5

setupMouseEvent(){
    this.uMousePos = new THREE.Vector3(0,0,0);

    this.raycastMesh = new THREE.Mesh(
      new THREE.PlaneGeometry(10,10),
      new THREE.MeshBasicMaterial()
    )
    window.addEventListener('pointermove', (e)=>{
      this.pointer.x = (e.clientX/this.width) * 2 - 1;
      this.pointer.y = -(e.clientY/this.height) * 2 + 1;
      this.raycaster.setFromCamera( this.pointer, this.camera );
      const intersects = this.raycaster.intersectObjects([this.raycastMesh]);
      if (intersects.length>0){
        // console.log(intersects[0].point);
        this.uMousePos = intersects[0].point;
       
      }

    })
  }

片段 6

 this.matFBO.uniforms.uMousePos.value = this.uMousePos;

片段 7

vec4 pos = texture2D(uPosTexture, vUv);
vec3 originPos = texture2D(uOriginPosTexture, vUv).xyz;
vec3 force = pos.xyz-uMousePos;

片段 8

vec3 posToGo = originPos + normalize(force)*forceFractor;
pos.xy += (posToGo.xy-pos.xy)*0.005;

片段 9

varying vec2 vUv;
uniform sampler2D uPosTexture;
uniform sampler2D uOriginPosTexture;
uniform vec3 uMousePos;


void main() {
    vec4 pos = texture2D(uPosTexture, vUv);
    vec3 originPos = texture2D(uOriginPosTexture, vUv).xyz;

    // color.x += 0.01;
    vec3 force = pos.xyz-uMousePos;
    float len = length(force);
    float forceFractor = 1./max(0.1,len*50.);
    vec3 posToGo = originPos + normalize(force)*forceFractor;
    pos.xy += (posToGo.xy-pos.xy)*0.005;
  
    gl_FragColor = vec4( pos.xyz, 1.0 );
}  

FAQ

這篇文章主要在介紹什麼?

這篇筆記整理 FBO technique,也就是在 WebGL 中用 framebuffer 與 render target 來模擬 compute shader 工作流的方式。

這篇文章適合誰閱讀?

適合想理解 ThreeJS 粒子特效筆記 03:FBO 技術 背後實作、設計取捨與學習脈絡的讀者。

Buy me a coffee