iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 18
2

今天要利用前面練習過的粒子系統,學習如何在網頁上實作 3D 爆炸的特效,並會透過 dat.GUI 來觸發爆炸、改變爆炸粒子尺寸、相機參數來測試更合適的視覺效果。同時會討論如何將已經消散的爆炸粒子移除並釋放記憶體,並分享實作爆炸時遇到的問題。

17_front_2s

Photo by Erwan Hesry on Unsplash


這是本系列第 17 篇,如果還沒看過第 16 篇可以點以下連結前往:

用 Three.js 來當個創世神 (16):專案實作#8 - 粒子特效場景


爆炸效果怎麼做

接下來要來研究如何利用粒子系統做爆炸效果,觀察一下預期的效果大概要長這樣:

17_creeper_explode

行為跟下雪不太一樣,一開始筆者也沒什麼頭緒,於是 google 找到這個 Codepen 範例,看起來是比較接近我們需要的效果。

稍微 trace 一下 code 後,大概可以理解他的邏輯如下:

  1. 先在某一個相同的爆炸源點建立一群粒子頂點
  2. 每個粒子有各自的運動方向(可視為是一個三維向量)
  3. 爆炸被觸發後,各粒子依循自己的爆炸方向位置一直疊加上去,直到超出視野則會看不到,會有向四面八方擴散的效果。

簡單說爆炸就是「沿著爆炸中心點讓粒子向四面八發隨機運動」就可以做到了,看起來很合理也不難做,但最後直覺應該要加一個回收機制,達成某特定條件後,將上一個消散的粒子群清掉,不然會產生一堆粒子散落在場景不可知的地方繼續飄移。

瞭解了原理後下面就開始來實作吧!

 

爆炸實作

17_demo

請參考完整原始碼成果展示,請點擊右上角 explosionTrigger 觸發爆炸。

友情提醒,爆炸中心可能有點亮,會畏光的讀者可以先將螢幕調暗保護眼睛,筆者已將 Demo 預設粒子尺寸先降至 20,看起來比較不刺眼,但似乎效果比較像煙火;大約需設到 100 以上,比較有厚實的爆炸感。

 

1. 建立粒子系統

// 變數宣告
const pointCount = 10000 // 粒子總數
const movementSpeed = 10 // 移速種子
let explosion // 爆炸物件
let size = 50 // 粒子尺寸
const textureLoader = new THREE.TextureLoader()
const smokeTexture = textureLoader.load('./smoke.png')
// 爆炸類別
class Explosion {
  constructor(x, y) {
  
  	 // 幾何體
    const geometry = new THREE.Geometry()

    // 材質
    this.material = new THREE.PointsMaterial({
      size: size,
      color: new THREE.Color(Math.random() * 0xffffff), // 顏色隨機
      map: smokeTexture,
      blending: THREE.AdditiveBlending,
      depthTest: false,
      transparent: true,
      opacity: 0.7
    })

    this.pCount = pointCount
    this.movementSpeed = movementSpeed
    this.dirs = []

	 // 建立粒子系統所需頂點
    for (let i = 0; i < this.pCount; i++) {
      const vertex = new THREE.Vector3(x, y, 0) // 每個頂點起點都在爆炸起源點
      geometry.vertices.push(vertex)
    }

    let points = new THREE.Points(geometry, this.material)

    this.object = points

    scene.add(this.object)
  }
}

這邊將爆炸寫成一個 class 物件,可以在之後要做多重爆炸效果時派上用場,建立粒子系統的方法都跟前面差不多,唯一不同的是這裡每個頂點的起點都設在同一點,才會有一群粒子從爆炸中心散射的效果。

 

2. 準備爆炸方向屬性

class Explosion {
  constructor(x, y) {
    ...
    for (let i = 0; i < this.pCount; i++) {
      ...
      const r = this.movementSpeed * THREE.Math.randFloat(0, 1)
      // 噴射方向隨機 -> 不規則球體
      const theta = Math.random() * Math.PI * 2
      const phi = Math.random() * Math.PI
      this.dirs.push({
        x: r * Math.sin(phi) * Math.cos(theta),
        y: r * Math.sin(phi) * Math.sin(theta),
        z: r * Math.cos(phi)
      })
    }
  }
}

這邊在爆炸物件中另外建立一個爆炸方向的屬性,取名為 dirs,是一個陣列,儲存每一個粒子的爆炸方向,而在前言中那個範例他將每個方向都設為 Math.random() * r - r / 2,會讓爆炸時的形狀類似正方體:

17_square

於是參考這篇文章做球體的方法,改寫成球體座標,並且將半徑用移動速度再乘上一個隨機值就能產生接近球體但比較像爆炸的不規則形狀。

 

3. 爆炸特效動畫

class Explosion {
  ...
  update() {
    let p = this.pCount
    const d = this.dirs
    while (p--) {
      let particle = this.object.geometry.vertices[p]
      particle.x += d[p].x
      particle.y += d[p].y
      particle.z += d[p].z
    }
    this.object.geometry.verticesNeedUpdate = true
  }
}
function render() {
  if (explosion) {
    explosion.update()
  }
  ...
  requestAnimationFrame(render)
  renderer.render(scene, camera)
}

在爆炸物件實作一個 method 用來做爆炸的動畫 update(),方法就是每個頂點依據自己的噴射方向屬性一直疊加,最後會漸漸淡出視野可見範圍,達到爆炸的效果。

 

釋放記憶體與驗證

由於一直產生一大群粒子系統消散後不清除,會是一件很佔記憶體的行為,所以參考官方提供將物體從場景中移除的正確寫法:

function render() {
  var geometry = new THREE.SphereBufferGeometry(...);
  var material = new THREE.MeshBasicMaterial(...);
  var mesh = new THREE.Mesh( geometry, material );
  scene.add( mesh );
  renderer.render( scene, camera );
  scene.remove( mesh );
  // clean up
  geometry.dispose();
  material.dispose();
  texture.dispose();
}

於是在我們的爆炸類別中添加一個移除粒子系統的 method 來做 dispose()

class Explosion {
  ...
  destroy() {
    this.object.geometry.dispose()
    scene.remove(this.object)
    // console.log(renderer.info) // 驗證
    this.dirs.length = 0
  }
}

並且在每次點擊 dat.GUI 觸發爆炸時去做 destroy

this.explosionTrigger = function() {
  if (explosion) {
    explosion.destroy()
  }
  explosion = new Explosion(0, 0)
}

但是不是真的成功釋放了,筆者找到一個驗證方法是把 renderer.info 印出來看:

沒做 destroy 的版本:

17_not_dispose

有做 destroy 的版本:

17_dispose

在觸發三次爆炸後,能觀察發現渲染器中始終都只有存在一個粒子系統,並且粒子數一直維持一萬顆。

 

其他遇到的問題

1. dat.GUI 修改粒子數與移動速度

在試著加粒子總數與移動速度的 dat.GUI 控制時,一開始加上去後會沒有效果,參考了其他人針對粒子總數用 dat.GUI 的寫法後,看起來在更新畫面前,要把舊的釋放掉,再重新建立一個新的粒子系統才會生效。

但試著照做後一直遇到原本粒子系統的 update 正在使用 pointCount 的問題,直覺是資料開始互相衝突了,開始體會到框架開發的方便性,之後要用 Vue 整合時再來解決這個問題,順便把爆炸及其他功能模組化,不然程式也是越來越大包了,可讀性開始變差。

2. 爆炸效果

觀察 Demo 會覺得爆炸的效果好像哪裡不太像,所以試著調整了爆炸方向的形狀,並且把相機的近平面距離利用 dat.GUI 調整比較高的參數,讓過近的爆炸粒子能直接消失,視覺效果會好一些。

3. 驗證是否成功移除粒子系統

上面有提到目前是藉由 renderer.info 來確認,但不確定這樣驗證正不正確,如果讀者有其他更正確的驗證方法,麻煩不吝指教,感恩!

 

今日小結

今天成功地練習了如何利用粒子系統來實作爆炸的效果,並試著使用 dispose() 來做記憶體的釋放,達到優化的作用,明天會將爆炸效果套到專案中的苦力怕自爆上,並試著模擬得更像一些,我們明天見!

最後再附上今天的完整原始碼成果展示

 

參考資料


上一篇
用 Three.js 來當個創世神 (16):專案實作#8 - 粒子特效場景
下一篇
用 Three.js 來當個創世神 (18):專案實作#9 - 爆炸特效
系列文
用 Three.js 來當個創世神31

尚未有邦友留言

立即登入留言