O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.

MH:W | GPU Particle - モンスターハンター:ワールドにおけるGPU Particleの実装

6.120 visualizações

Publicada em

「MH:W | GPU Particle モンスターハンター:ワールドにおけるGPU Particleの実装」米山哲平

First presented at Game Creators Conference 2018 (http://www.gc-conf.com/event_2018/).

Publicada em: Tecnologia
  • DOWNLOAD FULL. BOOKS INTO AVAILABLE FORMAT, ......................................................................................................................... ......................................................................................................................... 1.DOWNLOAD FULL. PDF EBOOK here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. EPUB Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. doc Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. PDF EBOOK here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. EPUB Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL. doc Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... ......................................................................................................................... ......................................................................................................................... .............. Browse by Genre Available eBooks ......................................................................................................................... Art, Biography, Business, Chick Lit, Children's, Christian, Classics, Comics, Contemporary, Cookbooks, Crime, Ebooks, Fantasy, Fiction, Graphic Novels, Historical Fiction, History, Horror, Humor And Comedy, Manga, Memoir, Music, Mystery, Non Fiction, Paranormal, Philosophy, Poetry, Psychology, Religion, Romance, Science, Science Fiction, Self Help, Suspense, Spirituality, Sports, Thriller, Travel, Young Adult,
       Responder 
    Tem certeza que deseja  Sim  Não
    Insira sua mensagem aqui

MH:W | GPU Particle - モンスターハンター:ワールドにおけるGPU Particleの実装

  1. 1. MH:W | GPU Particle モンスターハンター:ワールドにおけるGPU Particleの実装 株式会社カプコン 米山哲平 1
  2. 2. 講演時との差異 • 一部不要なスライドの削除 – ノートはなにかのヒントになるように残しておきます • 一部アニメーションの削除 – Slide Share等でそのまま見えるように • コード例等を追加 – 講演時は目が泳ぐので非表示にしていました • GCC中/Twitterの質問の回答を追加 – アイテム周りの具体的な構造 – SRV/UAVの使い分けによる最適化について 2
  3. 3. 講演内容 • GPU Particle システムの詳細 – コンピュートシェーダの処理フロー • コードを交えつつデータマップを中心に図解 3
  4. 4. 対象者 • GPU Particle 及び GPGPU に興味のある方 – プロ・アマチュア・学生を問いません • 複雑な数式は無し • HLSL(Compute Shader)の基礎知識があると読みやすい • はじめての人にも実装のヒントになるはず! 4
  5. 5. 登壇者紹介 • 米山 哲平 • 株式会社カプコン – 技術研究開発部 技術開発室 – 2013年入社 • 「モンスターハンター:ワールド」 エフェクトモジュールのメインプログラマ – 自社開発エンジン「RE ENGINE」のエフェクトモ ジュールにも携わった – 入社以前は個人でゲームエンジンを開発し、Microsoft Imagine Cup 2013 日本代表として選出 5
  6. 6. アジェンダ • 前知識 – GPU Particleとは? – MH:WのEffectとは? • 実装の前に – 実装背景/要求/方針 • 実装詳細 – 全体概要 – データ構造の詳細 – 各GPU処理の詳細 6 • TIPS – 可読性に関する話 – デバッグツール – 高速化案
  7. 7. GPU PARTICLE 前知識① 7
  8. 8. GPU Particle とは? 8 GPUを使って大量のエフェクトパーティクルを処理するシステム
  9. 9. GPUを使う利点 • 超並列 – 今やGPUは描画以外も計算できるように – ただし制限や癖がある 9 一般的なCPUは8スレッド前後 GPUでは1000スレッド以上で動作可能 (Stream Processor数換算)
  10. 10. 代表例1 • 導蟲で使用 – 光点の一粒一粒が個別に動作 – 導蟲単体で最大30,000パーティクル発生 10
  11. 11. 代表例2 • GPU Particleを一番使用しているモンスター 11
  12. 12. 代表例2 • 約100エミッター • 最大約50,000パーティクル – 1.00~1.50msで動作 12
  13. 13. EFFECT 前知識② 13
  14. 14. MH:Wでのエフェクト 専用のエディターで作成 14
  15. 15. エフェクトの構成 15 • エフェクト – エミッター1 • Velocityアイテム • Lifeアイテム – エミッター2 • Velocityアイテム • Scale Animation アイテム エフェクト エミッター1 エミッター2 アイテム
  16. 16. アイテムとパーティクル • パーティクルはアイテムに従って動かす – パーティクルはエミッター毎に複数発生 16 Scale Animation Rotate Animation Velocity
  17. 17. アイテムの具体的な構造 • 必要な機能のアイテムを追加 • アイテムごとにパラメータを設定 17 D&Dで追加
  18. 18. 複数のエミッター 18 火の粉エミッター 煙エミッター 炎エミッター
  19. 19. 実装の前に 要求/実装方針 19
  20. 20. 実装背景 • シェーダーコードでシステム構築 – GPU上で動作するのでデバッグが難しい – クラス等も使用できないので拡張が難しい 20
  21. 21. 実装背景 • 期間は(物量に対して)かなり短い – エフェクトシステムはフルスクラッチ • 必要な機能が揃ってない – GPU Particle, 雷表現, レーザー表現… – エフェクトアセットは並行して作成 21
  22. 22. 実装背景 • CPUに余裕がない – Dispatchによる描画コマンドの増大を防ぎたい • 処理を気軽にGPUに投げたい – アーティストにどんどん利用してほしい 22
  23. 23. 方針 • 可読性を第一に! – 保守コストを抑える – 高い拡張性で先の新規実装を簡単に • エミッター毎にDispatchしない! – CPUに空きがないので描画コマンドを少なくしたい – GPUの空き時間もできるだけ抑えたい • Interlock(Atomic)命令を0に! – GPUの並列性を阻害したくない 23
  24. 24. 全体概要 実装詳細① 24
  25. 25. 大まかな流れ 25 エミッター生成 エミッター用 データ領域を確保 パーティクル用 データ領域を確保 エフェクト生成 CPUで処理 エミッター更新 アイテム更新 エミッター用 データ領域の更新 フレーム開始 CPUで処理 GPUで処理 全パーティクル 一括更新 全パーティクル 一括ソート エミッター 管理情報更新 全エミッター 一括更新 エフェクトの描画 フレーム開始 パーティクル 新規追加
  26. 26. エフェクト生成時の流れ 26 Emitter Table 0 3 4 5 エミッター生成 エミッター用 データ領域を確保 パーティクル用 データ領域を確保 エフェクト生成 CPUで処理 Tableに登録 Emitter Header 0 1 Not Use 2 Not Use 3 4 5 Header領域のインデックスに相当 ※ Emitter Table等のデータ構造については後述します
  27. 27. Emitter BinaryEmitter Binary エフェクト生成時の流れ 27 エミッター生成 エミッター用 データ領域を確保 パーティクル用 データ領域を確保 エフェクト生成 CPUで処理 Emitter Header 0 1 Not Use 2 Not Use 3 4 5 Head Size Emitter Binary領域を 末尾から確保 Particle Binary
  28. 28. Particle BinaryParticle BinaryEmitter Binary エフェクト生成時の流れ 28 エミッター生成 エミッター用 データ領域を確保 パーティクル用 データ領域を確保 エフェクト生成 CPUで処理 Emitter Header 0 1 Not Use 2 Not Use 3 4 5 Head Size Particle Binary領域を 末尾から確保
  29. 29. CPUで毎フレーム行う処理 29 エミッター更新 アイテム更新 エミッター用 データ領域の更新 フレーム開始 CPUで処理 • 親の移動値の反映 等 ※パーティクルの座標ではない事に注意
  30. 30. CPUで毎フレーム行う処理 30 エミッター更新 アイテム更新 エミッター用 データ領域の更新 フレーム開始 CPUで処理 • アイテム単位の更新 – パーティクルを出す数の決定 – 外部パラメーターの反映 – タイムライン制御 …等
  31. 31. CPUで毎フレーム行う処理 31 エミッター更新 アイテム更新 エミッター用 データ領域の更新 フレーム開始 CPUで処理 • Headerから領域を参照 • アイテムを順番に書き込む Emitter BinaryEmitter Header 0 1 Not Use 2 Not Use 3 4 5 Head Size Velocity アイテム Scale Anim アイテム Rotate Anim アイテム
  32. 32. GPUで毎フレーム行う処理 32 Emitter Table 0 3 4 5 テーブルに登録されているエミッターを更新 GPUで処理 全パーティクル 一括更新 全パーティクル 一括ソート エミッター 管理情報更新 全エミッター 一括更新 エフェクトの描画 フレーム開始 パーティクル 新規追加
  33. 33. Particle HeaderParticle Header GPUで毎フレーム行う処理 33 GPUで処理 全パーティクル 一括更新 全パーティクル 一括ソート エミッター 管理情報更新 全エミッター 一括更新 エフェクトの描画 フレーム開始 新規パーティクルを末尾に追加 パーティクル 新規追加 Emt0 Emt3 Emt5 前のフレームのパーティクル
  34. 34. GPUで毎フレーム行う処理 34 GPUで処理 全パーティクル 一括更新 全パーティクル 一括ソート エミッター 管理情報更新 全エミッター 一括更新 エフェクトの描画 フレーム開始 Header上の全パーティクルを更新 パーティクル 新規追加 Particle Header
  35. 35. GPUで毎フレーム行う処理 35 GPUで処理 全パーティクル 一括更新 全パーティクル 一括ソート エミッター 管理情報更新 全エミッター 一括更新 エフェクトの描画 フレーム開始 生きてるパーティクル と 死んだパーティクル をソートで分離する パーティクル 新規追加 Particle Header Particle Header Sort Alive Dead
  36. 36. GPUで毎フレーム行う処理 36 GPUで処理 全パーティクル 一括更新 全パーティクル 一括ソート エミッター 管理情報更新 全エミッター 一括更新 エフェクトの描画 フレーム開始 各エミッター配下のパーティクルの分布を収集 Interlock命令を使用せずにパーティクル数をカウントするための重要な処理 パーティクル 新規追加 Particle Header Emitter Range 0 1 Not Use 2 Not Use 3 4 5
  37. 37. GPUで毎フレーム行う処理 37 GPUで処理 全パーティクル 一括更新 全パーティクル 一括ソート エミッター 管理情報更新 全エミッター 一括更新 エフェクトの描画 フレーム開始 描画 パーティクル 新規追加
  38. 38. データ構造編 実装詳細② 38
  39. 39. データ構造 • バッファは起動初期に大きなメモリ空間を確保 – エフェクトを再生するたびにメモリを確保し直さない • メモリの断片化防止 • そもそもメモリアロケーションはコストが高い 39
  40. 40. データ構造 • バッファの種類は大きく分けて2種類 – 毎フレームCPUからGPUへ書き込まれるバッファ • GPUからは読み取りのみ可能 – GPU内でのみ読み書きされるバッファ • CPUからは読み取りができない 40
  41. 41. CPUからGPUへ書き込まれるバッファ 41 Emitter HeadersEmitter Table Emitter Binary
  42. 42. Particle Binary Particle Headers Particle Index List GPU内でのみ読み書きされるバッファ 42 1/3
  43. 43. GPU内でのみ読み書きされるバッファ 43 2/3Particle Headers Emitter Range
  44. 44. GPU内でのみ読み書きされるバッファ 44 Particle Num Prev Particle Num Draw Indirect Args VertexNum / InstanceCount / VertexOffset / InstanceOffset Dispatch Indirect Args(X/Y/Z) 3/3
  45. 45. データの取り出し方 • スレッドごとにエミッターデータへアクセスする方法 • 簡易擬似コードでの書き方 – 実際にはGPUでアクセスするのでHLSLで書く必要がある – ここではイメージを掴んでもらう程度で・・・ 45 for (uint threadIdx = 0; threadIdx < EmitterNum; ++threadIdx) { uint emitterId = EmitterTable[threadIdx]; uint emtHead = EmitterHeaders[emitterId].EmtBinHead; void* emtPtr = &EmitterBinary[emtHead]; //emtPtrをキャストしてアクセス! //auto* item = static_cast<CommonItem*>(emtPtr); //emtPtr += sizeof(CommonItem); }
  46. 46. データの取り出し方 • 「パーティクルのバイナリ領域」と「エミッターのバイナリ領域」 にアクセスする方法 46 for (uint threadIdx = 0; threadIdx < TotalParticleNum; ++threadIdx) { auto& particle = ParticleHeaders[threadIdx]; auto& emitter = EmitterHeaders[particle.EmitterId]; uint ptHead = particle .Index * emitter.PtSize; void* emtPtr = &EmitterBinary[emitter.EmtBinHead]; void* ptPtr = &ParticleBinary[emitter.PtBinHead + ptHead]; /* emtPtrとptPtrでアクセス! */ }
  47. 47. メモ • データを脳内にマップするのが一番大変 – 慣れないうちは自分で理解しやすいマップ をエクセルとかで作るのが良い 47開発時のメモ画像
  48. 48. まとめ • Headerを使ってBinaryにアクセスする • データを脳内でマップするのが一番大変 – 慣れない間はエクセル等で早見表を作ろう 48
  49. 49. GPU処理フロー 実装詳細③ 49
  50. 50. GPU上の処理の流れ Clear System Clear Particles Bitonicsort Range Particles Terminate Particles Build Emitter Draw Args Build Primitive 50※概要一覧表は付録に用意してあります Fill Unused Index Buffer Spawn Particles Initialize Particles Update Particle Begin Update 初回初期化処理(2 pass) 毎フレーム実行される処理(10 pass)
  51. 51. GPU上の処理の流れ Clear System Clear Particles Bitonicsort Range Particles Terminate Particles Build Emitter Draw Args Build Primitive 51 Fill Unused Index Buffer Spawn Particles Initialize Particles Update Particle Begin Update 初回初期化処理(2 pass) 毎フレーム実行される処理(10 pass)
  52. 52. Clear System • アプリケーション起動後、初回のみ実行 • システム全体で共有される値を初期化 • Dispatch(1, 1, 1)で実行 [numthreads(1, 1, 1)] void ClearSystem() { IndirectArgs.Store(IA_PARTICLE_COUNTER, 0); IndirectArgs.Store(IA_PREV_PARTICLE_COUNTER, 0); } [compute shader] 52 1/12
  53. 53. Clear Particles • アプリケーション起動後、初回のみ実行 • 後述するパーティクル毎のキーとなる情報を初期化 – INVALID_TAG(0xffffffff or -1) [compute shader] 53 2/12
  54. 54. Clear Particles • Dispatch(ceil(TotalParticleMax / (float)PARTICLE_PER_THREAD), 1, 1) で実行 – つまりパーティクルの数分スレッドを実行 – 参考としてPARTICLE_PER_THREADはMH:Wでは256を設定しています [numthreads(PARTICLE_PER_THREAD, 1, 1)] void ClearParticles(uint3 id : SV_DispatchThreadID) { if (TotalParticleMax <= id.x) return; ParticleHeader[id.x].tag = INVALID_TAG; ParticleHeader[id.x].depth = 0.0f; } [compute shader] 54 2/12
  55. 55. GPU上の処理の流れ Clear System Clear Particles Bitonicsort Range Particles Terminate Particles Build Emitter Draw Args Build Primitive 55 Fill Unused Index Spawn Particles Initialize Particles Update Particle Begin Update 初回初期化処理(2 pass) 毎フレーム実行される処理(10 pass)
  56. 56. GPU上の処理の流れ Clear System Clear Particles 56 初回初期化処理(2 pass) Bitonicsort Range Particles Terminate Particles Build Emitter Draw Args Build Primitive Fill Unused Index Spawn Particles Initialize Particles Update Particle Begin Update 毎フレーム実行される処理(10 pass)
  57. 57. Begin Update • 「前のフレームの総パーティクル数」+「発生”予定”のパーティクル数」 →「”仮”の総パーティクル数」 • Dispatch(1, 1, 1)で実行 [compute shader] [numthreads(1, 1, 1)] void BeginUpdate() { uint prePtCount = IndirectArgs.Load(IA_PARTICLE_COUNTER); uint curPtCount = min(prePtCount + TotalSpawnCount, TotalParticleMax); IndirectArgs.Store(IA_PARTICLE_COUNTER, curPtCount); IndirectArgs.Store(IA_PREV_PARTICLE_COUNTER, prePtCount); } 57 3/12 Fill Unused Index Spawn Particles
  58. 58. Fill Unused Index • 新しく割り当てられたParticle Index List領域 をINVALID_TAG(-1 or 0xffffffff)で埋める – 新規エミッターが発生した際に実行 – 割当領域は必ず末尾から取られる • そのため範囲は連続しているので一括して埋められる [compute shader] 58 4/12 Spawn Particles Initialize Particles Begin Update
  59. 59. Spawn Particles • 新しく発生するパーティクル数(Spawn数)を決定する – 新規パーティクルはParticle Headersの末尾に割り当てる • ついでにEmitter Rangeも0で初期化 [compute shader] 59 5/12 13番目から4要素 Initialize Particles Update Particle Fill Unused IndexBegin Update
  60. 60. Initialize Particles • Spawnしたパーティクルにインデックスを割り当てる – Particle Index Listから未使用インデックスを取得 – Dispatch(iEmitterCount, 1, 1) で実行 • 1スレッドで複数パーティクルを処理する [compute shader] 60 6/12 使用中Particle Index Listの数と等しい Update Particle Bitonicsort Fill Unused Index Spawn Particles
  61. 61. Initialize Particles • Spawnしたパーティクルにインデックスを割り当てる – Particle Index Listから未使用インデックスを取得 – Dispatch(iEmitterCount, 1, 1) で実行 • 1スレッドで複数パーティクルを処理する [compute shader] 61 6/12 後ろからこの個数分割り当てる -1 の場合は現在のParticleNumの連番を割り当てる Update Particle Bitonicsort Fill Unused Index Spawn Particles
  62. 62. – 1スレッドで複数パーティクル分を処理する場合のコード例 • 256スレッドで処理する場合は0~255, 256~511, 512~767…という感じ – 最悪想定で1フレームに大量のパーティクルが出る可能性はある 62 for (uint i = threadIdx.x; i < emitterData.spawnNum; i += PARTICLE_PER_THREAD) { // TODO. } Initialize Particles [compute shader] 6/12 Fill Unused Index Spawn Particles Update Particle Bitonicsort
  63. 63. Update Particle • パーティクルをアイテムに従って動かしていく – 1スレッドで1パーティクル処理する [compute shader] 63 7/12 Scale Animation Rotate Animation Velocity Bitonicsort Range Particles Spawn Particles Initialize Particles
  64. 64. • 更新はParticle Headerを参照する – 生存している最小限のパーティクルだけ効率的に処理される 64 Update Particle [compute shader] 7/12 Bitonicsort Range Particles Spawn Particles Initialize Particles
  65. 65. Update Particle • Uber Shader的に全アイテム実行 • GPUで動かすコードとしては一見やばそう・・・ [compute shader] 65 7/12 Bitonicsort Range Particles Spawn Particles Initialize Particles 次のページで説明
  66. 66. Update Instance Item • 殆どのアイテムで同じ書き方 – 読み出して、更新して、書き出す という一連の流れ – 実際にアイテムが設定されてない限り実行されない • stream****関数は後述 66 次のページで説明
  67. 67. Update Item 67 • アイテムごとの処理のみ • BufferのLoad/Storeとは完全に分離
  68. 68. Bitonic Sort • Particle Headersを並び替え – の順番で並び替える • Indexは見ない [compute shader] 68 8/12 Alive, EmitterID, Depth Range Particles Terminate Particles Initialize Particles Update Particle
  69. 69. Bitonic Sort • 同時に深度ソートも行う – MH:Wでは半透明エフェクトが多いので深度ソートが必要 – 加算エフェクトだけに制限した場合は深度ソートが不要になる 69 Z-Sortなし 半透明描画 前後関係がおかしくて気持ち悪い Z-Sortあり 半透明描画 ボリューム感が有る Z-Sortなし加算 違和感はない [compute shader] 8/12 Range Particles Terminate Particles Initialize Particles Update Particle
  70. 70. Bitonic Sort • Bitonicsortを選択した理由 – アルゴリズムを理解していたからというだけ – 全体をソートできれば何でも良い – Bitonicsortの注意点 • パーティクル数に応じてDispatch回数が増加 – 再生中の全エフェクトのMax Particle Numの合計から計算 » 実際の生存パーティクル数がCPU上から見れないため • ここだけ同期命令の一つである GroupMemoryBarrierWithGroupSyncを使用 – 並列度を下げるものではない [compute shader] 70 8/12
  71. 71. Sort Key Key1 Alive 1bit パーティクルの生存フラグ Emitter ID 13bit パーティクルの親エミッターの番号 Particle Index 18bit EmitterIDと合わせれば直接パーティクルにアクセスできるように Key2 Depth 32bit カメラからの距離(float) 71 • Particle Headersは1要素64bit – Shared Memoryに一旦読み込む場合に丁度いいサイズ – AliveとEmitterIDをまとめてuint値として比較出来る • 比較ではParticle Indexを使用しない
  72. 72. 比較関数 72 • Aliveは実際にはInvalidフラグな点に注意(trueの場合に無効) – 最上位ビットなのでKey1をuintとして見た場合に非常に大きな値になる • Key1をキーとして昇順に並び替えると無効化されたパーティクルは後ろに移動する • 結果的にINVALID_TAG(0xffffff)を使用すると勝手に末尾へ移動する
  73. 73. Range Particles • ソートしたParticle Headersから各エミッター配下のパーティクルの境界を調べる • これを利用すればカウントアップ等をする必要がなくなりInterlock命令が排除できる – (End – Head)で簡単にパーティクル数が求まる [compute shader] 73 9/12 これを構築するプロセス Terminate Particles Build Emitter Draw Args Update Particle Bitonicsort
  74. 74. Range Particles • 各スレッドで2要素ずつ取り出して比較するだけ – 隣接している要素のEmitter IDが違うならデータの境界ということ [compute shader] 74 9/12Particle Headers Terminate Particles Build Emitter Draw Args Update Particle Bitonicsort
  75. 75. Range Particles [compute shader] 75 • それだけだと抜けが出るので一要素ずらして再比較 – 各スレッドで2+1要素を比較するだけで全パーティクルの分布が求まる 9/12Particle Headers Terminate Particles Build Emitter Draw Args Update Particle Bitonicsort
  76. 76. Terminate Particles • 死んだパーティクルのIndexを返却する [compute shader] 76 10/12 ※ここの値は実際には不定 返却済み領域だけ正しくなる Build Emitter Draw Args Build Primitive Bitonicsort Range Particles
  77. 77. Build Emitter Draw Args • Emitter Rangeから最終的なパーティクル数を求めEmitter Dataを更新 [compute shader] 77 11/12 End - Alive Range Particles Terminate Particles Build Primitive
  78. 78. Build Emitter Draw Args • Draw Indirect用の引数を構築 – MH:WではEmitter毎にDraw Callを行っている – x6しているのはビルボードの場合の頂点数(正確にはインデックス数) [compute shader] 78 VertexNum / InstanceCount / VertexOffset / InstanceOffset 11/12 Range Particles Terminate Particles Build Primitive
  79. 79. Build Primitive • パーティクル情報から頂点を構築 – 1スレッドで1パーティクル分実行 – ビルボード及びリボン形状をサポート [compute shader] 79 • Color • Size • Rotation • Scale Anim • Rot Anim • UV Sequence • etc… Build… BillboardParticle 12/12 Terminate Particles Build Emitter Draw Args
  80. 80. GPU上の処理の流れ Clear System Clear Particles 80 初回初期化処理(2 pass) Bitonicsort Range Particles Terminate Particles Build Emitter Draw Args Build Primitive Fill Unused Index Buffer Spawn Particles Initialize Particles Update Particle Begin Update 毎フレーム実行される処理(10 pass)
  81. 81. まとめ • 各Dispatchは基本的に単純なことしかしない • Emitter RangeによってInterlockで実装しがちな カウントアップ処理を賄う 81
  82. 82. データーの整頓 実装詳細④ 82
  83. 83. Emitterの追加 • Emitterが追加されるとデータ領域が末尾に割り当てられる 83 Emt0 Emt1 Empty Emptybuffer Emt0 Emt1 Emt2 Emt3 New! New! buffer
  84. 84. Emitterの削除 • 削除する際は領域を開放する – しかしバッファーは最初に確保した大きなメモリ空間だけなの で断片化してメモリが枯渇する 84 Emt0 Emt1 Emt2 Emt3 Delete! buffer Emt0 Empty Emt2 Emt3buffer
  85. 85. Bufferの詰め直し • エミッターが削除された場合はデータ領域を詰め直す必要がある – ガベージコレクション的なメモリ再配置処理 • このためにダブルバッファにする必要がある 85 Copy Copy Emt0 Emt1 Emt2 Emt3 Delete! buffer0 Emt0 Emt2 Emt3 Emptybuffer1
  86. 86. Bufferの詰め直し対象 • Particle Index List – Copy Sub Resourceを何回か呼び出す必要がある • Particle Binary – こちらはUpdate Particle時の書き出し先に新しい領域を指定すれば良い • 簡単に済ませたいならCopy Sub Resourceでの実装でも良い 86
  87. 87. Emitter削除時の注意 • Emitterの削除要求が発生したら1フレーム待機して 必ず配下のパーティクルを削除するように! – Particle Headersに配下のパーティクルが残っている可能性 87 Alive EmittertID ParticletID TRUE 0 0 TRUE 1 1 TRUE 1 2 TRUE 1 3 TRUE 1 4 TRUE 2 0 TRUE 2 1 TRUE 3 0 EmtID EmtBinHead 0 0 1 ??? 2 1088 3 2348 4 ??? 5 ??? Particle Headers Emitter Headers 不正な領域にアクセス!
  88. 88. 実装結果 88
  89. 89. 処理速度 • 約100エミッター / 最大50,000パーティクル – 1.00~1.50msで動作 – 実際に生存しているパーティクルは平均25,000パーティクル • ソート処理によって必要最小限の更新が行われる 89
  90. 90. 処理速度 • Async ComputeによってZ-Prepassの裏で動作 – 処理負荷はほぼ隠蔽される 90 Graphics Pipe Compute Pipe
  91. 91. GPU Particleの実装期間 • 約1ヶ月 – シェーダー及び管理システムを含む • エディターはCPU実装版と互換性をとりそのまま使用 • この時点で基本的なアイテム/機能まで実装 – 読みやすいコードを心がけたのでサクサク書けた • コードの可読性に関しては後述 91
  92. 92. TIPS 92
  93. 93. If分岐に関して • If分岐使いまくりだけど大丈夫? – とくにUpdate Particle 93
  94. 94. If分岐に関して GPUはif分岐に弱い If(threadIdx % 2) Process A Process B スレッド開始 END 実行マスク ↓↓↓↓↓↓↓↓↓↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓↓↓↓↓↓↓↓↓↓ 両方実行したときと 同じサイクル数がかかる 94
  95. 95. If分岐に関して • Particle Headerを参照しているため 隣接スレッドは基本同じエミッターの配下 – つまりアイテム構成も同じ • バラバラな分岐はあまり起こらない 95
  96. 96. コードの可読性 • できるだけコードを統一したい • 冗長で規則性のあるコードをまとめたい • コピペミスなどを事前に防ぎたい 96
  97. 97. コードの可読性 • 構造体をByteAddressBufferで管理する場合で問題に – 冗長 – Load/Storeする場合はほぼ同じコードを書く必要がある – コピペミスしやすい 97 struct PtVelocity { float3 velocity; float gravityAccume; float gravity; };
  98. 98. Stream Function • HLSL上であらゆる型の Load/Store を統一的 に記述するためのヘルパー関数 98 void streamPt_Velocity3D(uint rw, inout uint offset, inout PtVelocity value) { streamPt(rw, offset, value.velocity); // float3 streamPt(rw, offset, value.gravityAccume); // float streamPt(rw, offset, value.gravity); // float } struct PtVelocity { float3 velocity; float gravityAccume; float gravity; };
  99. 99. Stream Function • 中身はread/writeによって分岐してるだけ 99 void streamPt(uint rw, inout uint offset, inout float3 value) { if (rw == readOnly) stream_read(srv, offset, value); else if (rw == read) stream_read(uav, offset, value); else if (rw == write) stream_write(uav, offset, value); else stream_none(offset, value); }
  100. 100. これ大丈夫なの? • SRV/UAV , Load/Store命令が混在してるが・・・ • rwに”定数”(read/readOnly/write/none)を指定すればOK – コンパイル時最適化で競合する命令は全部消える! – If分岐命令も全部消える! 100 void streamPt(uint rw, inout uint offset, inout float3 value) { if (rw == readOnly) stream_read(srv, offset, value); else if (rw == read) stream_read(uav, offset, value); else if (rw == write) stream_write(uav, offset, value); else stream_none(offset, value); }
  101. 101. Stream Function • あとは基本形のオーバーロードを用意 101 void stream_read(ByteAddressBuffer buffer, inout uint offset, out float value) { value = asfloat(buffer.Load (offset)); offset += 4 * 1; } void stream_read(ByteAddressBuffer buffer, inout uint offset, out float2 value) { value = asfloat(buffer.Load2(offset)); offset += 4 * 2; } void stream_read(ByteAddressBuffer buffer, inout uint offset, out float3 value) { value = asfloat(buffer.Load3(offset)); offset += 4 * 3; } void stream_read(ByteAddressBuffer buffer, inout uint offset, out float4 value) { value = asfloat(buffer.Load4(offset)); offset += 4 * 4; }
  102. 102. Stream Function • read/write定数を切り替えるだけ – “定数”というところが大事 102 PtVelocity velocity; // 読み取り streamPt_Velocity3D(read, read_offset, velocity); // 更新 updateVelocity3D(velocity); // 書き込み streamPt_Velocity3D(write, write_offset, velocity);
  103. 103. Stream Function • プリプロセッサマクロにしてあるので任意の名前で 全基本型のオーバーロードメソッドが用意できる 103 STREAM_R_RW(streamPt, ParticleBinary, ParticleBinaryUAV);
  104. 104. Sizeofに応用 • Noneを指定すればノーコストで構造体のsizeofもできる – HLSLだと基本型しか出来ないけどこれで解決! – noneだとoffsetを進めるだけなのでコンパイル時最適化で最終的な定数しか残らない! – C++側とHLSL側で定義している構造体サイズが一致してるかのチェックも簡単に! 104 uint sizeof_Velocity3D() { uint size = 0; PtVelocity temp = (PtVelocity)0; streamPt_Velocity3D(none, size, temp); return size; }
  105. 105. コンパイラを信じよう! • 分かりやすくシンプルなコードを – 一つのローカル変数を共有するのはほぼ無意味 • 細かい粒度で関数化しよう – HLSLでは殆どの場合インライン展開される – Load/Store命令は連続していればまとめられる • Stream Functionを信じよう! 105
  106. 106. デバッグツール GPU Particle Profiler 106
  107. 107. デバッグツール • エミッター毎の使用領域を色分け 107
  108. 108. デバッグツール • 大量のパーティクルを出してるエミッターを調査 – 処理負荷調査に役立つ 108
  109. 109. デバッグツール • データーの不整合も監視 – システムのバグ調査に非常に役立つ 109
  110. 110. デバッグツール • できるだけ最初に用意して! – バグ調査でかなりの時間を奪われる • 不整合が起こるとドライバハングにつながる – デバッグツールで予兆を監視しよう! • 実装後すぐにバグを見つけることが出来た 110
  111. 111. 高速化案 • 加算ブレンドエフェクトに制限する – メリット • ソート処理を無くす事が可能 – デメリット • 内部では全パーティクルを常に動作させる必要がある 111
  112. 112. 高速化案 • エミッター単位でソート – メリット • パーティクル数が一定以下なら1Dispatchでソート可能 • 加算ブレンドエフェクトの場合はソートを省ける – デメリット • Dispatch数が増大 • パーティクル数に気をつける必要がある 112
  113. 113. 高速化案 • Update Particleを複数に分ける – メリット • 並列実行性能を上げやすい – 細かい粒度で動作させられる – 使用レジスタを抑えやすい – デメリット • 細かすぎるとオーバーヘッドが増えて逆効果 113
  114. 114. 高速化案 • アイテムの値をシェーダーに焼き込む – メリット • Emitter Binaryから読み込む必要がないので爆速 – 最適化もかかるので10倍位早くなるかも・・・ – デメリット • Dispatch数が増大 • シェーダーファイルが増大 • アイテムのパラメータを動的に変更できない • 焼き込みシステムを構築する必要がある 114
  115. 115. 最後に • 簡単ではないが超難しいわけではない – システムの規模が大きいぶんやることは多い – データ構造を脳内でちゃんとマップできれば行けるはず! • みんなもGPU Particle実装しよう! – かっこいいエフェクトを作ろう! – パーティクル管理のシステムは応用が効くはず! 115
  116. 116. おしまい 116
  117. 117. 今回の内容 • 前知識 – GPU Particleとは? – MH:WのEffectとは? • 実装の前に – 実装背景/要求/方針 • 実装詳細 – 全体概要 – データ構造の詳細 – 各GPU処理の詳細 • TIPS – コードの可読性について – デバッグツール – 高速化案 117 Clear System Clear Particles Bitonicsort Range Particles Terminate Particles Build Emitter Draw Args Build Primitive Fill Unused Index Buffer Spawn Particles Initialize Particles Update Particle Begin Update
  118. 118. 付録 118
  119. 119. PHASE 説明 ClearSystem システム変数を全て初期化します。 アプリケーション起動後、初回にのみ実行。 ClearParticles Particle Headerの全領域をInvalid Tagで埋める。 アプリケーション起動後、初回にのみ実行。 BeginUpdate 更新を始めるためにシステム変数を準備。毎フレーム実行。 FillUnusedIndexBuffer このフレームでエミッターが追加された場合に実行。 新しく割り当てられたParticle Index Listを-1で埋める。 SpawnParticles 有効な全エミッターをなめてエミッター毎にデータを構築。 InitializeParticles 新しく発生したパーティクルのヘッダ情報を初期化。 このフレームで生成されるパーティクルのヘッダ情報は Particle Headersの末尾(未使用領域の先頭)に追加される。 UpdateParticle Core Updateと呼んでいる部分。 設定されているアイテムによってパーティクルを動かします。 具体的にはVelocityアイテムで加速させたり、 Lifeアイテムによって半透明にしたりします。 さらにLifeが0になったり特定条件でパーティクルが死んだ場合、 ヘッダのAliveフラグをFalseにします。 Bitonicsort Particle Headersを並び替えます。 パーティクル数によって複数回Dispatchされます。 RangeParticles ソート済みのParticle Headersから各エミッター毎に、 最終的な生存パーティクル数、このフレームで死んだパーティクル数、 さらに全エミッター合計の生存パーティクル数を算出します。 もちろんInterlock無し。 TerminateParticles 死んだパーティクルのIndexを、 Particle Index Listに返却します。 BuildEmitterDrawArgs エミッター毎に実行されるDrawIndirectの引数を構築 BuildPrimitive パーティクルからビルボードとかリボンの頂点を作成 119
  120. 120. Q.SRV/UAVの使い分けによる最適化について 120 GCC中の質問 A.実際には使い分けています。 – 講演中は混乱しないように省いてました
  121. 121. Q.SRV/UAVの使い分けによる最適化について 121 GCC中の質問 • STREAM FUNCTIONマクロでも対応済み – STREAM_R_RW(name, srv, uav) • SRV/UAVを併用して読み書き – STREAM_RW(name, uav) • UAVのみで読み書き – STREAM_R(name, uav) • SRVのみで読み取りのみ
  122. 122. Q.SRV/UAVの使い分けによる最適化について 122 GCC中の質問 • STREAM_R_RW(name, srv, uav)を使用した場合 – readOnlyでsrv読み出し – readでuav読み出し – writeでuav書き込み • Dispatch中のバッファの使用用途で使い分けてます void streamPt(uint rw, inout uint offset, inout float3 value) { if (rw == readOnly) stream_read(srv, offset, value); else if (rw == read) stream_read(uav, offset, value); else if (rw == write) stream_write(uav, offset, value); else stream_none(offset, value); }
  123. 123. Q.一番好きなエフェクトは? A.テオのスーパーノヴァ 123 GCC中の質問

×