28.5K Views
September 10, 19
スライド概要
2019/9/5に開催されたCEDEC2019の講演スライドです。
講師:山村達彦(ユニティ・テクノロジーズ・ジャパン合同会社)
:大下岳志(ユニティ・テクノロジーズ・ジャパン合同会社)
Unityのイベント資料はこちらから:
https://www.slideshare.net/UnityTechnologiesJapan/clipboards
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
Unityではじめるオープンワールド制作 エンジニア編 Tatsuhiko Yamamura @ Unity Takeshi Oshita @ Unity
Unityではじめるオープンワールド制作 広域で大量のオブジェクトを扱うオープンワールド風フィールドをUnityで、 エンジニア 編 アーティスト編 どう作る? どう動かす? /2
オープンワールド風な世界を表現したい 広いステージ 多いオブジェクト スムーズな ロード・アンロード
を、モバイルで。
DOTS • ECS • • オブジェクトの管理と処理の制御 JobSystem • • (Data-Oriented Technology Stack) 並列処理 Burst • 高速なアセンブリを出力するコンパイラ •連続して同じ処理を行いたい •素早くオブジェクトを集めたい •並列で処理したい •データを効率的に処理したい
最終的な結果 広いステージ 1km ✕ 1kmの広域を表現 多いオブジェクト 合計20万 Entity (4万LOD) スムーズな ロード・アンロード メインスレッドのスパイク2ms以下
LODの切り替えが酷いのは自分の手抜き
なお… • 超広域マップにおける誤差問題は 今回はノータッチ • ECSベースの 物理演算(Unity Physics)は未使用 ColliderはGameObjectで配置 • キャラクターの挙動制御は 従来のMonoBehaviourを使用
構成はハイブリットECS GameObject キャラクター挙動 ステージロード プレイヤー オブジェクト配置 座標 物理演算 個別制御の 実装が簡単 ECS カメラ制御 UI 描画 大量のオブジェクト 制御に強力
今回の話 1.ECSベースのシーンへ変換 2.シーンをロード 3.広範囲を描画 4.その他、モバイル対策
GameObjectからECSへ変換 • 大量のGameObjectは嫌なので、 Entityに変換 • シーン上に配置したオブジェクトの 配置を、そのまま使用したい • Sceneエディターを使いたい
ECSとGameObjectではデータが異なる Entity GameObject Transform Position Scale Rotation Mesh Filter Mesh Mesh Renderer Material Shadow Lighting Translation Rotation Scale Transform Matrix RenderMesh
対応するコンポーネントへコンバート GameObject Entity Transform Translation Position Scale Rotation Conversion System Rotation Scale Mesh Filter Transform Matrix Mesh Mesh Renderer Material Shadow Lighting Conversion System RenderMesh
GOからEntityへ変換するシステムを用意 変換するGO一覧から 「Light」を持つモノを抽出 LightComponentを 作り中身を埋める Entityに LightComponentを登録
パッケージの幾つかはコンバーターに対応 Hybrid Renderer UnityPhysics 変換前 変換後 Rigidbody PhysicsBody Collider PhysicsShape MeshRenderer RenderMesh LODGroup MeshLODGroup Component Light LightComponent
ConvertToEntityでGOからEntityへ変換 • GameObjectをEntityへ変換 • 既存のコンポーネントを活用する場合は コチラを使用する e.g. Translationの位置でAnimatorのパラメーターを変更する等。 TranslationはECSで計算し、結果をAnimatorに注入する
Game Disk Editor ConvertToEntityワークフロー GameObjects Convert Entities/ Component GameObjects Scene GameObjects GameObjects Convert Entities/ Component • 実行時にGameObject毎に変換処理 • 量が多いと困る事も
SubSceneワークフロー • エディターでGameObjectをEntityへ変換 • ECSのEntity/Componentエディターという立ち位置も • SubScene以下のオブジェクトは 全てEntityへ変換される
GameObjects Disk GameObjects Scene Game Editor SubSceneワークフロー GameObjects Convert Entities/ Component
GameObjects Disk GameObjects Scene Game Editor SubSceneワークフロー GameObjects Convert Entities/ Component Save SubScene GameObjectを変換し SubSceneとして保存
Game Disk Editor SubSceneワークフロー GameObjects エディターで結果を確認するため 現在のワールドにオブジェクトを登録 Entities/ Component Merge GameObjects Scene GameObjects SubScene Load Entities/Components GameObjectをデシリアライズせず 画面のプレビューが可能
Editor SubSceneワークフロー GameObjects GameObjects Convert Entities/ Component Save Disk Scene Game SubSceneの内容を変更したい場合、 元となるGameObjectを編集 GameObjects SubScene
Game Disk Editor SubSceneワークフロー GameObjects Entities/ Component GameObjects Scene SubScene GameObjects Entities/ Component Load Entities/Components Merge ゲームプレイ時も 同じデータを使用
サブシーン変換手順
変換後のファイル SubSceneデータ本体 SubSceneが参照するデータ (大体がアセットを参照する用途)
全部を含めると広域をロードしすぎる・・・ Root OBJ 1 OBJ 2 OBJ 3 OBJ 4 OBJ 5 OBJ 6 OBJ 7 OBJ 8 …
グリット毎にGOを分割してSubSceneに変換 SubScene (0, 0) SubScene f (0, 1) SubScene SubScene f f (1, 1) (2, 1) SubScene SubScene f f (1, 2) (2, 2) OBJ 1 OBJ 2 Root2 OBJ 3 OBJ 4 Root3 OBJ5 OBJ 6 Root4 OBJ 7 OBJ 8 … SubScene f (0, 2) SubScene SubScene (1, 0) (2, 0) Root1
ゲームプレイ開始までの時間も短く • All Game Object 約 1分35秒 • ECS 約 8秒 ECSでもSubScene編集時は 開いているオブジェクトの数だけ 起動時間が伸びる
コンバーターは無ければ変換されない GameObject Translation Transform Convert Rotation Scale Mesh Filter Mesh Renderer Transform Matrix • 変換先がなければ コンポーネントは パフォーマンスに 影響を及ぼさない • オブジェクトに拡張 用コードを登録しても 特に問題は無い Convert Renderer TreeTagC Convert Grid LOD Generator TreeTag
Entity ≒ オブジェクト? Entity Translation Rotation Entity Translation Rotation Collider Player Entity Translation Rotation Collider Collider Entity Translation Rotation Collider
Entity毎にコンポーネントが纏められるように見えるが… Entity Entity Entity Entity Entity Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Collider Collider Collider Collider Collider Rigidbody Rigidbody var pos = EntityManager.GetComponentData<Translation>(entity);
オブジェクト間のメモリは連続して置かれる Translation Translation Translation Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Rigidbody Rigidbody Rigidbody Rigidbody Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Renderer Renderer Renderer Renderer Renderer Renderer Renderer Renderer Enemy Enemy Enemy Tree Tree Tree Tree Player
実態は構造体の配列 Array[] Array[] Translation Translation Translation Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Rigidbody Rigidbody Rigidbody Rigidbody Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Renderer Renderer Renderer Renderer Renderer Renderer Renderer Renderer Enemy Enemy Enemy Tree Tree Tree Tree Player
EntityはデータにアクセスするためのID Entity Array[] Array[] Translation Translation Translation Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Rigidbody Rigidbody Rigidbody Rigidbody Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Renderer Renderer Renderer Renderer Renderer Renderer Renderer Renderer Enemy Enemy Enemy Tree Tree Tree Tree Player
配列にIDを渡して参照するイメージ TransformMatrix [ Entity ] Translation Translation Translation Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Rigidbody Rigidbody Rigidbody Rigidbody Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Renderer Renderer Renderer Renderer Renderer Renderer Renderer Renderer Enemy Enemy Enemy Tree Tree Tree Tree Player
コンポーネントの組み合わせでチャンクを分割 実際にはChunkという単位で分割 Chunk Chunk Chunk Translation Translation Translation Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Rigidbody Rigidbody Rigidbody Rigidbody Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Renderer Renderer Renderer Renderer Renderer Renderer Renderer Renderer Enemy Enemy Enemy Tree Tree Tree Tree Player
チャンク単位でのアクセス 「特定の組み合わせ」を持つデータを素早く取得 Chunk Chunk Chunk Translation Translation Translation Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Rigidbody Rigidbody Rigidbody Rigidbody Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Renderer Renderer Renderer Renderer Renderer Renderer Renderer Renderer Enemy Enemy Enemy Tree Tree Tree Tree Player
SubSceneの中身 Entities Chunk Archetype Chunk Archetype Type Manager Chunk Chunk • Chunk • Entity • Archetype • Shared Component Data (データ構造) (チャンク内のデータにアクセスするキー) (Entityが持っているデータの組み合わせ) Archetype Shared Component Data (複数のEntityから参照できるデータ)
SubSceneの中身 SubScene Entities Chunk Archetype Chunk Archetype Type Manager Chunk Chunk • Chunk • Entity • Archetype • Shared Component Data (データ構造) (チャンク内のデータにアクセスするキー) (Entityが持っているデータの組み合わせ) Archetype Shared Component Data (複数のEntityから参照できるデータ)
“実際にゲームでロードする流れ”
どうしたいか Load • • ロードとアンロードを早くしたい 早ければ、近くでロードしても間に合う メインスレッドに対して負荷を掛けたくない Loaded SubScene ゲーム進行を止めたくない UnLoad
どうやってロードするのか? • SubSceneのAutoLoadSceneにチェックを入れる (簡単) OR • SceneDataを持つEntityにRequestSceneLoadを追加 (距離に応じて段階的にSubSceneをロード・アンロードしたい場合等) or
Terrain&他 3.7秒 Terrain&他 GameObject 8.2秒 & SubScene 0.8秒
約20万Entity メインスレッドの負荷 約2ms以下 ※ShaderCompile、Colliderの生成は除く
ロードすべきもの SubScene Entities Chunk Archetype Chunk Archetype Chunk Chunk Archetype Shared Component Data アセット郡 Prefab
チャンクのロード Archetype Archetype Disk Chunk Chunk Chunk
チャンクのロード ほぼコピーするだけ Archetype Archetype Disk Chunk MemCopy Chunk Chunk Chunk
ChunkはSubScene毎に独立している チャンクが独立しているので、 既にあるChunkに値をマージはしなくても良い SubSceneA Chunk SubSceneB Chunk Chunk Chunk SharedComponentData SharedComponentData SubSceneID (A) SubSceneID (B)
マネージドメモリを挟まずデータをロード
アセットのロード SharedComponentData RenderMesh RenderMesh RenderMesh SubScene • Shared Component Dataが参照する Prefabを経由してアセットを参照 • 通常と同様のアセット読込 Prefab Assets
読込はAyncUploadPipelineを活用 AUP無し Strage 非同期読込 VRAM Memory アップロード完了まで待つ ヒープに読み終わるまで待機 AUP Strage 非同期読込 Memory リングバッファ分を読込 固定タイムスライスで 少しずつアップロード VRAM
Entityのロード Entities Staging World 使用できるEntityに間隔がある Main World Entities
EntityのIDをリマップ Entities Staging World Remap Main World Entities
Main WorldにEntityをマージ Entities Staging World Remap Main World Entities
SubSceneを非同期でロード Main World Staging World Staging World Staging Wor
SubSceneを非同期でロード Load Request Main World Staging World Staging World Staging Wor
SubSceneを非同期でロード Main World Staging World Staging World Load Async Load Shared Components Load Async Load Entities メモリ的に独立してるので 別スレッドでも無問題 ECS Data Staging Wor
SubSceneを非同期でロード Main World Staging World メインワールドに統合 マージだけなので 短期間で終わる Staging World Load Shared Components Load Entities Move ECS Data Staging Wor
SubSceneのアンロード SubSceneIDを参照しているチャンクを開放する SubSceneA Chunk SubSceneB Chunk Chunk Chunk SharedComponentData SharedComponentData SubSceneID (A) SubSceneID (B)
“ロード終了、次は描画周り”
たくさんのオブジェクトを表現したい • 全体で4万LOD • 遠い距離のオブジェクトも表現 • ついでにポリゴンも多い (初期画面、1200万ポリゴン)
ハイブリットレンダラー • ECSで使えるレンダラー • 同じタイプのメッシュを連続して描画(バッチング) • カリング、LOD、HLOD付き • MeshRendererとMeshFilterからの変換 • 描画はBatchRendererGroupを使用
MeshRendererを変換したEntity Entity Translation Rotation Transform Scale LocalToWorld Matrix Transformから得られる行列 Render Bounds 描画判定の大きさ WorldRender Bounce 実際の描画判定 RendererMesh 描画するメッシュの情報
描画のバッチング Translation Translation Translation Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Scale Scale Scale Scale Scale Scale Scale Scale Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds RendererMesh RendererMesh RendererMesh RendererMesh RendererMesh RendererMesh RendererMesh RendererMesh
描画のバッチング RenderMeshの種類でチャンクを分割 Chunk Chunk Translation Translation Translation Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Scale Scale Scale Scale Scale Scale Scale Scale Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds RendererMesh RendererMesh
描画のバッチング 一気に描画(最大1023インスタンス) Chunk Chunk Translation Translation Translation Translation Translation Translation Translation Translation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Rotation Scale Scale Scale Scale Scale Scale Scale Scale Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Transform Matrix Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds Render Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds RendererMesh Render Batch RendererMesh Render Batch
描画したいオブジェクトが交互に配置されている RenderMesh … 異なるメッシュの草が交互に配置
通常の描画 見える範囲 青を描画 … 赤を描画 青を描画 赤を描画 青を描画
RenderBatchで描画している場合 Chunk バッチングが優先される RendererMesh(青) 青を描画 青を描画 青を描画 … 赤を描画 赤を描画 Chunk RendererMesh(赤)
Instancingが有効なら、よくまとまる Chunk RendererMesh(青) … ※半透明の描画は 注意が必要 青をまとめて描画(Instancing) 赤をまとめて描画(Instancing) Chunk RendererMesh(赤)
ステージのオブジェクトは (ほぼ)動かない 動かない • 親子構造で動く事は無い • バウンディングボックスを 更新しなくても良い 動かない 動く 動かない
オブジェクトの親子構造 Root Grid1 LOD LOD Grid2 LOD0 LOD1 LOD2 LOD0 LOD1 LOD2 ワールド座標を決める為に 親の座標を知る必要 描画されるべき範囲を決めるため ワールド座標が必要
オブジェクトの親子構造をフラットに Translation Translation Translation Translation Rotation Rotation Rotation Rotation Scale Scale Scale Scale Transform Matrix Transform Matrix Transform Matrix Transform Matrix Render Bounds Render Bounds Render Bounds Render Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds RendererMesh Transform Matrix Transform Matrix Transform Matrix Transform Matrix WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds RendererMesh
ステージの親にStaticOptimizeEntityを追加 Root Grid1 LOD LOD Grid2 子オブジェクトに対して最適化 LOD0 LOD1 LOD2 LOD0 LOD1 LOD2
チャンク単位でバウンディングボックスを設定 Transform Matrix Transform Matrix Transform Matrix Transform Matrix WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds Transform Matrix Transform Matrix Transform Matrix Transform Matrix WorldRender Bounds WorldRender Bounds WorldRender Bounds WorldRender Bounds ChunkBounds RendererMesh RendererMesh
ChunkBounds ChunkBounds ChunkBounds ChunkBounds ChunkBounds ChunkBounds チャンク毎に 描画範囲を持つ ChunkBounds
ChunkBounds ChunkBounds チャンク毎に 描画するか 判断できる ChunkBounds ChunkBounds ChunkBounds ChunkBounds ChunkBounds
ChunkBounds ChunkBounds ChunkBounds ChunkBounds ChunkBounds インスタンス毎のカリングで 画面外を除外 ChunkBounds ChunkBounds
できるだけ広範囲を描画したい 遠くの小さいオブジェクトも表現
視界の範囲を描画 描画されないオブジェクト 描画されるオブジェクト 視界
遠距離を描画すると負荷が跳ね上がる 見える範囲が広がると 描画回数も大幅に増える
遠距離の描画 結合すれば描画命令は抑えられるが、 描画すべき項目が増えてしまう…
遠距離の描画(HLOD) 遠距離では結合したメッシュを表示 1 描画命令 1 描画命令
遠距離の描画 カメラを近づけると HLOD結合前のモデルになる
HLODの構築 HLOD用モデルはエディター拡張で作成 HLOD 0 LOD 0 LOD 1 LOD 2 HLOD 1 LOD 0
HLODの段階的ロード SubScene RenderMesh LOD RenderMesh RenderMesh LOD HLOD RenderMesh RenderMesh RenderMesh LOD RenderMesh RenderMesh LOD HLOD RenderMesh RenderMesh LOD
HLODの段階的ロード SubScene RenderMesh LOD Section RenderMesh RenderMesh セクションで分割 必要に応じてロード LOD HLOD RenderMesh RenderMesh RenderMesh LOD RenderMesh RenderMesh LOD HLOD RenderMesh RenderMesh LOD
Sectionの例 HLOD0ロード済 HLOD0を未ロード
ハイブリットレンダラーの問題点 • バッチの特性上、透明は描画順で問題を起こす (奥の透明が手前に描画される事も) • Bakeしたライトマップは使えない LightProbeとリアルタイムシャドウを使う • BatchRendererGroupが SRP Batcherと相性が悪い
“モバイルで動かそう”
動かしてみた • CPU的には超余裕 • 発熱やばい • GPUがやばい iPhoneX
頂点シェーダーの負荷 • Vertex Shaderに72ms • だいたいCutoutのせい • 絵的に許せる物はShaderをTransparentへ • 草をポリゴン切り抜きにしたかった(希望 • トライアングルをLODで減らす 12M → 4Mへ • LODのバイアス 2 → 1
LOD化されてないメッシュはUnityMeshSimplifireを使用 https://github.com/Unity-Technologies/UnityMeshSimplifier
Impostors • ビルボード • mpostorsを使うと、頂点数を 大幅に減らせる • モデルによっては絵が 破綻する事も
フィルレート • 解像度を減らそう(SRPで) • UIだけ高解像度、ゲーム画面は低解像度
Graphics Jobs RenderThread • 描画処理を 複数のスレッドに分割 RenderThread WorkerThread WorkerThread WorkerThread
発熱対策 • 放置すると、あっさり発熱アップ • 熱が上がるとパフォーマンスが下がる • レンダリング頻度をへらす機会を作る • OnDemandRendering.renderFrameIntervalを使う (SRPでレンダリングをスキップする、PlayerLoopを操作する等でも可)
• GPUを動かさなければ発熱はかなり抑えられる • ScreenSpaceOverlayのUIはGPUを動かし続ける点に注意 タイミングを見つけてフレームスキップ
まとめ • 広いステージ • • 多いオブジェクト • 広域描画時に使えるHLOD 早いロード・アンロード • 積極的な処理のスキップ • ステージの段階的なロード・アンロード • 高速でスパイクの少ないロード処理 • 多くの処理の並列化 • 効率的なバッチ処理
まとめ 1.オブジェクトをシーンに配置 2.SubSceneに変換 SceneViewで配置するスタイルでも 結構高速で動くモノが作れる 3.SubSceneをロード 4.チャンク単位でバッチ描画 5.HLODで描画負荷を減らす データの塊を動かすスタイルで 色々と面白い最適化が出来る
まとめ ゲーム内で「量が多いオブジェクト」はECSに変換すると効率的 それ以外は今まで通りGameObjectベースで開発してOK! ECSはデータの管理を便利にしてくれる。 ただ、自分でメモリ管理できるならば、 JobSystemとBurstだけ活用するのも良い選択肢