Help us understand the problem. What is going on with this article?

【Unity】Unite 2015「Rederer Massive Amount of Objects in Unity」レポート

More than 3 years have passed since last update.

レポート一覧

講演ファイル

https://docs.google.com/presentation/d/1xeu4nnEtFqyJD68SSUTqIZleUoC2aCk3cGpG0kxvfg4/edit?usp=sharing
http://japan.unity3d.com/unite/unite2015/files/DAY1_1700_room2_Ishibashi.pdf

本講演が目指すところ

  • Unityで
  • いろんなトリックを用いて
  • 多数のオブジェクトを
  • 効率的に描画&更新する

  • 検証はほぼPC上

  • PS4/XboxOneくらいのスペックを推定

Topics

  • レンダリング
  • Graphics.DrawMesh()
  • 疑似インスタンシング

前提

  • もっともストレートな方法:1つ1つGameObject化
  • 10,000ともなると1つ1つGameObjectとして扱うのは非現実的

Graphics.DrawMesh();

  • Meshを描画するリクエストをrender queueに積むAPI
  • ただし、batchingは効かない
  • 数百単位であれば対処可能だが、万単位は厳しい
  • drawcallを抑えつつ大量のオブジェクトを描く手段が必要
    • 疑似インスタンシング
    • ハードウェアインスタンシング

疑似インスタンシング

  • 1つのMeshに多数のモデルを格納することで1回のDrawMesh()で多数のモデルを描く
  • 65,000モデルの頂点数を1回のdrawcallで書ける
  • 65,000=1つのMeshがもてる最大頂点数
  • 各インスタンスの情報をGPUに送り、頂点シェーダで各モデルを変形

TRS行列など

  • 1つのMeshに多数のモデルを格納
  • 元の頂点データを繰り返す
  • 何番目のモデルか、という情報をどこかに付与(例ではuv2)
  • 必要なbatchの数だけMaterialも用意
  • この数+モデルIDがインスタンスIDになる
    • 例:Cubeを10000個書きたい場合、10000/2708で4つ
  • 各MaterialにインスタンスIDの開始番号を付与
  • 必要数を超えたモデルは頂点シェーダで画面に出ないように加工
  • 1個書きたい場合でも格納されているモデルの数分処理される
  • vertex.xyz = 0.0

インスタンス情報をGPUへ

  • 何通りかの方法があり、一長一短がある
1. テクスチャにプラグインから書き込む
2. テクスチャにMesh経由で書き込む
3. ComputeBugger

テクスチャにプラグインから書き込む

  • RenderTexture(ARGBFloat)を使用
  • Textur2DはFloatのフォーマットをつかえない
  • テクスチャにデータを書き込むプラグインを用意
  • Texture.GetNativeTexturePtr()でテクスチャオブジェクトを取得
  • ネイティブAPIを用いてデータを書き込む
  • 頂点シェーダからはtex2Dlod()でデータを取得
  • 速いがプラグインを描く必要あり

テクスチャにMesh経由で書き込む

  • データ転送用シェーダを用意
  • データ用RenderTextureをレンダーターゲットに指定
  • Meshにインスタンス情報を書き込む
  • 注意点:vertices, uv以外は暗黙的にnormalizeされたりする
  • Graphics.DrawMeshNow()
  • UnityのRender queueを経由しない
  • 遅いがとてもポータブル

疑似インスタンシング ComputeBuffer

  • ComputeBufferは現状Direct3D11&PS4のみ対応
  • 任意のデ-タ構造の配列を格納可能
  • データの更新はComputeBuffer.SetData()を呼ぶだけ
  • 頂点シェーダからはStructuredBuffer<>としてアクセス可能
  • 環境を選ぶが早くて簡単

ハードウェアインスタンシング

  • 1回のDrawCallで1つのモデルを複数描く機能
  • 頂点シェーダの入力にインスタンス別のデータが加わる
  • TRS行列などを渡す
  • 頂点データは1モデル分で済む
  • Direct3D9~,OpenGL 3.1~,OpenGL ES 3.0~, WebGL 2.0~
  • UnityではGraphics.DrawProcedural()が該当
  • ただし、いろいろ使いづらい仕様になっている
  • Direct3D11対応環境/PS4でしか使えない
  • Meshをそのまま描くことはできない
  • 頂点シェーダの入力は頂点IDとインスタンスIDのみ
  • Unityのrender queueに載せることができない
  • 読んだその時点で即座に描かれる
  • surface shaderが使えない

対処方法

  • Meshのデータをそのまま渡すことはできない
  • ComputeBufferに頂点データを格納することで対処可能
  • 頂点シェーダで頂点IDとインスタンスIDから実データを参照
  • Unityのrender queueに載せることができない
  • 独自のRenderTextureに描いて結果をマージすることはできる
  • surface shderが使えない
  • deferredであればG-Bufferさえ生成できればライティングは共通処理を使える

  • Unity5.1でCommandBuffer.DrawProcedural()が追加

  • これによりrender queueに載せられるようになる

  • G-Bufferの生成、ライティングしないオブジェクトには必要十分

Graphics.DrawProeduralIndirect();

  • 描くインスタンスの数をComputeBufferから読み取れる
  • GPU Particleなどに最適

疑似インスタンシング:応用

  • DrawProcedural()っぽいことをDrawMesh()で実現
  • 頂点IDとインスタンスIDのみを格納したMeshを用意
  • それらを元に頂点シェーダで実データを取得して描画

まとめ

  • 良いところ
    • 今現在のUnityで使える
    • Unityのrender queueに載せられる
    • surface shaderを使える
  • 悪いところ
    • 頂点データが肥大化する
    • 超過分のモデルも処理される

Update

  • 万単位のオブジェくとをどうUnity上で扱い、どう更新するか
  • オブジェクトのデータはstructの配列として持つ
  • UnityのColliderとのインタラクションなどは独自に実装
  • MeshCollider以外の実装それほど難しくない
  • 当たった場所にAddForce()することでRigidBodyに力を伝達

いくつかの実装アプローチ

  • ネイティブコード
  • ComputeShader
  • C#でがんばる

ネイティブコードで実装

  • 数値計算の類はC#は苦手
  • ネイティブコードで適切に実装すれば10倍以上は速くなる
  • マルチスレッド化
  • SIMD化
    • データ構造の最適化(SoA化)
  • C/C++のほか、HPC向け言語も有力な選択肢
  • Inter ISPC
  • OpenCL(IntelがCPU実装やコンパイラを用意)
  • 描画用データはRenderTextureに格納
  • Texture.GetNativeTexturePtr()
  • MonoのAPIを用いる方法もある
  • C++からC#のUnityEngineのAPIを呼ぶ
  • ComputeBufferにデータを移すには現状これしかない
  • Monoを介するため低速

ComputeShader

  • PC,PS4/XBoxOneで有力な選択肢
  • Unity 5.1でOpenGL系の環境も対応
  • ゲームロジックには影響しないエフェクト類に最適
  • パーティクルの更新から描画までGPUで完結できるため高速
  • ゲームロジックに影響するものにはやや不向き
  • CPU側にデータを転送するのが大きなロスになる
  • GPU側のスペックの上限下限の幅の広さも問題
  • ComputeShaderならではのリッチな表現
  • G-Bufferを利用したスクリーンスペース当たり判定

C#でがんばる

  • プラットフォームによってはこれ以外選択肢がない
  • オブジェクトのデータはstructの配列として持つ
  • できるだけclassを参照しない書き方にする
  • classは参照するだけで結構な負担になる
  • 大きなデータのコピーは遅いので極力避ける
  • ThreadGroup.QueueUserWorkItem()による並列化
  • いわゆるタスク並列
  • 注意点:Unityの機能の大部分はメインスレッド以外からは触れない
  • メインスレッドで必要なデータを集めておくなどの工夫が必要
  • 最新の.NETである程度改善されている

まとめ

  • 現状大量描画は疑似インスタンシングが有力な選択肢
  • ハードウェアインスタンシングはUnity5.1以降に期待
  • アップデート処理は可能な限りC#を避けつつ自力で頑張る
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした