iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 16
0

You know nothing, Jon Snow. 在實作粒子系統的動畫效果時,踩到了個大坑,本篇將會繼續完成前一天的雪花飄落動畫,並分享實作中遇到的問題及可以優化效能的部分。

15_front

Photo by Zack Sharf on IndieWire


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

用 Three.js 來當個創世神 (14):粒子系統 - 雪花 Part.1


雪花動畫

今天將接續昨天的靜態雪花場景,來將飄落的雪花動畫完成:

15_demo

請參考完整原始碼成果展示

 

動畫效果實作

function createPoints() {
  ...
  for (let i = 0; i < particleCount; i++) {
    ...
    point.velocityX = THREE.Math.randFloat(-0.16, 0.16) // 粒子橫向移動速度
    point.velocityY = THREE.Math.randFloat(0.1, 0.3) // 粒子縱向移動速度
    ...
  }

  points = new THREE.Points(geometry, material)
  scene.add(points)
}

// 雪花落下循環動畫
function pointsAnimation() {
  points.geometry.vertices.forEach(function(v) {
    v.y = v.y - v.velocityY
    v.x = v.x - v.velocityX

    if (v.y <= -250) v.y = 250
    if (v.x <= -250 || v.x >= 250) v.velocityX = v.velocityX * -1
  })

  points.geometry.verticesNeedUpdate = true // 告訴渲染器需更新頂點位置
}

這邊先在建立每一個頂點的地方加上兩個屬性,分別是 x 與 y 軸的移動速度,x 軸為模擬雪花橫向飄動的速度,介於(-0.16, 0.16)間,而 y 軸模擬雪花落下的速度,介於(0.1, 0.3)之間。

並且在 render() 中實作一個動畫函式處理動畫的邏輯,遍歷所有粒子的頂點位置,讓每個粒子沿著各方向的速度運動,當打到底部及左右的邊界時,底部的粒子會直接循環回最頂部,而在左右邊界的粒子會往反向繼續飄落。

最後要加上 points.geometry.verticesNeedUpdate = true 這行,告訴渲染器需更新頂點位置,動畫才會有效果。

友情提醒:實作內容到此已完成,以下為開發過程中,遇到問題的筆記,請耐心服用或直接跳至今日小結。

粒子系統動畫大坑

儘管動畫邏輯看起來不難,但還是踩到了個大坑,就是如果沒寫 points.geometry.verticesNeedUpdate = true 這行,那動畫就不會動,過程中還懷疑是不是 Three.js 版本問題、或是需要使用 BufferGeometry 等等原因,抽絲剝繭再參考其他範例怎麼做動畫的,把 points.geometry 印出來才發現還真有個屬性叫 verticesNeedUpdate 預設是 false

原因是因為,在一般的幾何體中並不需要頻繁的更新頂點位置,所以為了節省效能,幾何體中的 verticesNeedUpdate 這個屬性預設都是 false,但在粒子系統中需要「操作頂點位移來達到動畫效果」時,就需要告訴 renderer 說:「嘿!幫我更新一下!」,它才會重新去渲染新的頂點位置。

深入地分析可以參考這篇文章:浅谈three.js中的needsUpdate

 

關於其他踩到的雷

另外記錄一下因為做粒子系統動畫效果時卡關這麼久的 debug 過程:

1. 動畫不連續 - Demo

由於遇到前面動畫一直不會動的問題,仔細地參考了官方雪花的範例,並參考它在動畫處的寫法:

for (let i = 0; i < scene.children.length; i++) {
  var object = scene.children[i]
  if (object instanceof THREE.Points) {
    object.position.y -= 0.16
    if (object.position.y < 0) {
      object.position.y = 100
    }
  }
}

是成功讓雪花飄落了,但卻會遇到全部粒子一同在某一瞬間位移的問題,仔細一看才知道官方的範例是控制五大群不同貼圖的粒子系統,讓它們有不同的運動才這樣寫,所以意思是這樣改會控到一整群的粒子,因此才會大家一起位移。

於是改參考這個很華麗的粒子流的寫法,使用 BufferGeometry 來改寫才終於正常,也才發現這個 needsUpdate 好像是關鍵,然後才回去試原本的寫法,果然只是需要設定屬性:

function render() {
  const array = points.geometry.attributes['position'].array
  let offset = 1
  for (let i = 0; i < particleCount; i++) {
    array[offset] -= THREE.Math.randFloat(0.1, 0.3)
    if (array[offset] < -250) array[offset] = 250
    offset += 3
  }
  points.geometry.attributes.position.needsUpdate = true
  ...
  requestAnimationFrame(render)
  renderer.render(scene, camera)
}

2. 堆積特別多的一層 - Demo

動畫正常後遇到的另一個問題,怎麼會有某一層特別厚,而這個原因就比較蠢一點,前面說過每一個頂點的初始位置是將(x, y, z)值分別設定在介於(-250, 250)的隨機值,但因為一開始在動畫循環寫法如下:

function pointsAnimation() {
  points.geometry.vertices.forEach(function(v) {
    v.y = v.y - 0.5
    if (v.y <= 0) v.y = 250
  })
  points.geometry.verticesNeedUpdate = true
}

眼尖的讀者應該可以容易地找出 bug,就在 if (v.y <= 0) v.y = 250 這行,沒錯,底部邊界是在 -250,也就是說設成 0,一開始就會有一半左右的粒子被循環回頂點,所以才會有一層特別厚。

 

可優化的部分

1. BufferGeometry

在實作中筆者使用 Geometry 這個幾何體來做為粒子系統的外殼,它相當的方便,因為它具有許多基本屬性像是 vertices、faces、colors 等等,但其實有個更節省記憶體與 GPU 計算的幾何體常被拿來做粒子系統,那就是 BufferGeometry

但其實 BufferGeometry 寫起來有點沒那麼友善,需要什麼屬性要自己加:

function createPoints() {
  const geometry = new THREE.BufferGeometry()
  const vertices = []

  material = new THREE.PointsMaterial({
    size: 5
  })

  const range = 500
  for (let i = 0; i < particleCount; i++) {
    const x = Math.random() * range - range / 2
    const y = Math.random() * range - range / 2
    const z = Math.random() * range - range / 2

    vertices.push(x, y, z)
  }

  // 加個 position 屬性記錄各頂點位置
  geometry.addAttribute(
    'position',
    new THREE.Float32BufferAttribute(vertices, 3)
  )

  points = new THREE.Points(geometry, material)
  scene.add(points)
}

但它也的確可以節省很多計算上的資源,等哪天專案開始風扇一直轉的時候可能要考慮優化這部分一下。

參考文章:Why is the Geometry faster than BufferGeometry?

這篇文章在討論有人實驗發現 GeometryBufferGeometry 還快,但大部分回覆者都表示 Geometry 會配置更多記憶體、在 Three.js 內部機制最後仍會被轉換成 BufferGeometry、之後可能會移除 Geometry 等問題。

2. 頂點更新動畫 - CPU 計算 vs GPU 計算

在 google 前面說的大坑時,找到這篇問答:Best way to update THREE.Points Geometry,看起來透過更新頂點位置的方法是使用 CPU 計算,如果能用 shader 改寫的話直接讓 GPU 計算應該在效能上會更好。

 

今日小結

這兩天完成了粒子系統中一個經典的雪花效果,並且透過載入貼圖及更新頂點的方法,一路跌跌撞撞終於成功創造出一場大雪,也利用開發途中遇到的一些問題學習粒子系統的觀念,明天會利用這幾天的練習,將更多的粒子特效套用到遊戲的場景中,我們明天見!

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

 

參考資料


上一篇
用 Three.js 來當個創世神 (14):粒子系統 - 雪花 Part.1
下一篇
用 Three.js 來當個創世神 (16):專案實作#8 - 粒子特效場景
系列文
用 Three.js 來當個創世神31

尚未有邦友留言

立即登入留言