開発
開発
プラットフォームを選択

オープンワールドゲームとアセットストリーミング

更新日時: 2024/12/09
大規模なオープンワールドゲームを開発する上で中心となる課題のひとつは、そのワールドを実行時にメモリ内にどうフィットさせるかを見定めることです。開発者がデザインする世界には無数のメッシュ、素材、効果音、アニメーション、イベントがあります。一度に全体を読み込もうとすると、すぐにシステムのメモリが限界に達するでしょう。どんなにマシンが強力であってもいずれはこうした限界に達してしまいます。特に、Meta Questのようなハードウェアに制約があるターゲットの場合は、たちまち限界が訪れます。
幸いなことに、プレイヤーが体験するのは、ワールド全体のごく小さな一部分に過ぎません。高層ビルのような環境を考えてみてください。階が何百もあり、部屋も何千とあるかもしれませんが、プレイヤーが一度にやり取りするのは1つか2つの階だけです。屋外環境の場合も同様に、プレイヤーがやり取りするのは環境内の身近な部分に過ぎません。
つまり、どちらの例においても、ゲームのアセットを最大限の忠実度で読み込む必要があるのは、プレイヤーがやり取りする可能性がある場合のみです。一方で、遠くにあるものは低い忠実度で読み込むか、全く読み込まなくてよいことになります。これはつまり、プレイヤーがワールドの中を移動するにつれて、読み込んだ過去の領域を解放して新しい領域のためのスペースを作ることができるということでもあります。
メモリの問題を解決することはそう簡単ではないかもしれませんが、正しい方向へ進む第一歩として、このサンプルプロジェクトがお役に立てば幸いです。このプロジェクトでは、アセットの評価(Oculus StudiosのタイトルDead & Buried 2による)、ゲームの実行時パフォーマンスのプロファイリング、さまざまな詳細度(LOD)でのアセット生成、プレイヤーの位置に基づいてそれらのLODを読み込んだり解放したりするシステムの作成、ゲーム内のアセット管理システムが実際に動作していることを確認するためのツールの作成について説明します。そして最後に、エクスペリエンスの実行時パフォーマンスをさらに改善する方法についての提案をいくつか示します。
このサンプルは読み込みレベルジオメトリに焦点を当てたものですが、実際のプロジェクトでは、さまざまなゲームオブジェクトとその無数のコンポーネント(音声、アニメーション、追加のメッシュなど)を処理しなければなりません。それらを自分で工夫する場合にも、この記事で紹介する基本理念を応用できるはずです。つまり、さまざまな詳細レベルでアセットを準備しておき、必要以上に読み込まないようにし、1つのフレームにコストのかかる処理を詰め込み過ぎないようにするという考え方です。

アセットの複雑度の評価

アセット最適化を始める前に、アセットに関する理解を深めておく必要があります。このサンプルは、環境の複雑度を低減することに重きを置いています。複雑度は主に、環境を構成するメッシュとそうしたメッシュで使用されているマテリアルやシェーダーの複雑さに起因します。

メッシュの複雑度

Mesh Complexity
シーンの中のメッシュの複雑度を調べる最も簡単な方法は、エディターでワイヤーフレームを有効にすることです。このシーンでは、メッシュの複雑度はかなり良好に見えます。ほとんどの三角形が多くのピクセルをカバーしていますが、階段と教会にはとても細い三角形が確認できます。遠くにあるアセットでは細かい部分まで見分けられないので、それらは複雑度の低いLODにしても良いでしょう。
現在のビューの中の三角形のカウントを得るため、統計ウィンドウを有効にしてください。このビューには525,000個の三角形が表示されています。これは高い数値です。このビューではエディター専用プリミティブや未使用のUI要素がまだレンダリング中であり、オクルージョンカリングシステムが使われていないことを念頭に置いてください。ゲームでは、三角形カウントはこれよりもずっと低い値になると見込まれます。
Meta Questの三角形カウントのドキュメントには、ヘッドセットあたりの三角形カウントの目標数が記載されていますが、大きなオープンワールドレベルでは、三角形カウントの目標数の50%をターゲットにすべきです。このような目標数は、小さく囲まれたレベルのレンダリングにかかる負荷を基に設定されているためです。比較的小さく、閉鎖的なレベルでは、描画距離が短く、オクルージョンカリングがより効果的になるため、ジオメトリがより複雑になることがあります。カリング後、多くの三角形は、技術的にはビューフラスタム内にあったとしてもレンダリングされなくなります。大きくてオープンなレベルの場合は、一般にカリングの効果が低くなるため、ジオメトリの複雑度を調整する必要があります。これらいずれの場合も、シェーダーの複雑度に大きな影響を受けることを覚えておいてください。
最後に、画面上の小さな三角形の量を削減することが重要です。それにより頂点カウントが下がるだけでなく、MSAAが有効な場合にレンダリングされる余分のサンプル量が少なくなります。

シェーダーの複雑度

Shader code
シェーダーの複雑度を調べるため、まずコードを詳しく見てみましょう。この場合、シェーダーが極めてシンプルであることはすぐに分かります。直接ライティングは皆無です。すべてのライティングはライトマップをソースとしているので、いくつかのテクスチャーサンプルと若干のスカラー積だけでフラグメントの色を決定できます。
Shader disassembly and statistics
シェーダーの複雑度のもっと正確な測定値を得るには、プロジェクトブラウザーでシェーダーを選んでD3Dのための[Compile and show code(コードのコンパイルと表示)]を選びます。それにより、シェーダーのディスアセンブリといくつかの基本的な統計情報が出力されます。ディスアセンブリは、人が読み解くことは困難ですが、シェーダーが複雑なほど多くの演算が必要になるため、コストがかかるシェーダーを簡単に確認できます。一般的な規則として、統計値が大きく、またディスアセンブリコードの行数が多いと、シェーダーは低速になります。

ドローコールの測定と低減

GPUから最高のパフォーマンスを引き出す上で優先度が高いのは、フレームごとに必要なドローコールの数を減らすことです。詳しくは、以下の記事をご覧ください。
これらの資料を読めば、次に進む準備が完了です。
View of a level
これは、レベルの1つのサブセクションから見たビューであり、レベルの最も南側から北側を眺めた場合です。Unityでは、同じマテリアルとプロパティを共有する複数のドローコールを組み合わせて、「静的バッチ」と呼ばれるものが構成されます。このビューでは、ほとんどあらゆるドローコールの間にsetpass呼び出しがあり、その呼び出しによってこれらの静的バッチが崩れてしまっていました。これはライトマップによって引き起こされたものです。このレベルには合計12のライトマップがあり、近くにあるオブジェクトであってもライトマップが異なることが少なくありませんでした。
Draw calls with original lightmaps
それらのメッシュを強制的に同じライトマップにすることにより、setpass呼び出しの数を減らすことができました。これは、ライトマップパラメーターアセットで実現しました。このアセットを使えば、システムタグパラメーターを設定して、複数のオブジェクトを1つのグループに割り当てることができます。また、ライトマップパラメーターアセットを使うオブジェクトが生成できるライトマップの数を制限することもできます。地形をライトマップ0に、そして残りのメッシュをライトマップ1にしました。この場合は、これでライトマップの見栄えが良くなりました。同時にsetpass呼び出しの数が148からわずか16に減りました。バッチの合計数は156から94に減りました。
Draw calls with optimized lightmaps
さらに、ライトマップの設定で[Lightmap Resolution (ライトマップ解像度)]パラメーターを小さくすることにより、ライトマップの数を減らすこともできます。この場合効果は同じですが、制御可能な範囲が小さくなります。

メッシュバッチ処理とLOD生成

注: メッシュバッチ処理とは、複数のメッシュを組み合わせて1つの大きなメッシュにすることです。静的バッチ処理とは、Unityでのドローコールのバッチ処理のことです。静的バッチではsetpass呼び出しが1回ですが、ドローコールは複数回になることがあります。バッチ処理メッシュのドローコールは1回だけです。
ここでのレベルは、複雑なメッシュとモジュール型メッシュの混合を使ってビルドされたものです。しばしば建物では複雑なメッシュが使われ、それらに比較的小さなモジュール型部品を使って修飾が施されます。レベルのいくつかの部品は、完全にモジュール型部品のみで構成されています。メッシュをバッチ処理しない場合、ドローコールやsetpass呼び出しを数千回行うことになります。また、いずれのメッシュにもLODがないため、オブジェクト自体が画面上にほとんど表示されないにもかかわらず、メッシュが最大限の複雑度で使われることになります。
Scene view showing a mix of large building meshes, medium logs and tent, and small ice details.
このスクリーンショットが示すように、建物と岩は大きなサイズのメッシュになっています。木の丸太とテントのサイズは中程度であり、ドアに付いた氷は小さいサイズです。
また、シェーダーで直接ライティングは使われていません。ライティングはすべてライトマップに由来します。ライトマップによって静的バッチが崩れることがあるため、可能であればメッシュで同じライトマップを使いましょう。1つのメッシュのすべてのLODで同じライトマップを共有することも重要です。そうすることで(ライトマップ数の削減により)メモリ使用量を節約でき、LOD切り替え時のライトマップの矛盾が解消され、ライトマップのベイクがLODごとではなく1回だけで済みます。アセットにさまざまなLODがある場合、そのライトマップUVを必ずLOD0(詳細度最高)メッシュと一致させてください。

目標

Unity profiler rendering window
これは、Unityプロファイラーによってレンダリングされたウィンドウの開始時点のスクリーンショットです。このビューのドローコールは4105、setpass呼び出しは129、静的バッチは342であり、レンダリングされた三角形は140万を数えます。
オリジナルのQuestでは、GLES APIを使う場合、該当レベルでの静的ジオメトリのドローコールを100未満にし、ダイナミックドローコールをすべて含めて合計でドローコールが200前後になるようにターゲット設定すべきであることが経験上分かっています。Quest 2をターゲット設定しているか、Vulkan APIを使っている場合は、これらのターゲット値を若干大きく設定できます。
ターゲットとする三角形カウントは、シェーダーの複雑度に応じて異なります。この例のフラグメントシェーダーは極めてシンプルであるため、GPUには多くのヘッドルームが残されています。それにより、多くの三角形を保持できます。オリジナルのQuestでは、ライトマッピング、単一の平行光源からの直接ライティング、および法線マップとスペキュラーマップを備えたフラグメントシェーダーを使う場合、三角形の数が30万前後に限定されることが経験上分かっています。この例では、ライトマッピングだけがあるので、100万を超える三角形を簡単に処理できます。
特に注意を払う必要があるのが、setpass呼び出しです。setpass呼び出しは、レンダリングスレッドでは、特にGLESを使用している場合には非常にコストがかかることがあります。setpass呼び出しは、ドローコールとドローコールの間でマテリアルが切り替わるたびに発生します。ここでの目的は、当該レベル内のすべての静的ジオメトリに対してsetpass呼び出しを1回だけ設けることです。ただし、通常のジオメトリとは極めて異なるシェーダーを必要とするスカイボックスや地形などについては例外です。これを実現するには、テクスチャーを組み合わせて1つのテクスチャーアトラスまたは1つのテクスチャー配列にし、メッシュとシェーダーに変更を加えて適切なテクスチャー座標からサンプリングするようにします。
この例では、4つのサブレベルを組み合わせたものを反復して、1つの大きなワールドを創造しました。各サブレベルを個々にベイクできるようにするためです。そうすることで、サブレベルごとに異なるLOD設定を使えるようになります。また、サブレベルの1つに変化がある場合は、4つ全部ではなく1つのサブレベルをベイクするだけで済みます。

アプローチ

サブレベルごとに、マテリアルのベイク、ライトマップメッシュの生成、ライトマップのベイク、LODの生成、そしてLOD構造の作成という一連のステップを実行します。

マテリアルのベイク

このステップでは、全LODメッシュで使われる1つのマテリアルを作成します。オリジナルのマテリアルで使われるさまざまなテクスチャーを組み合わせるため、テクスチャーアトラスを利用できました。レベルごとの固有テクスチャーの数はあまり多くないので、一切品質を落とすことなくすべてのテクスチャーを1つの4096x4096テクスチャーに入れることができたのです。
4096x4096テクスチャーアトラスで全テクスチャーを格納できない場合は、テクスチャーを組み合わせてテクスチャー配列にすることを検討してください。新しいテクスチャーが生成されたら、メッシュのテクスチャー座標に変更を加えて、適切なテクスチャーUVからサンプリングされるようにすることができます。

ライトマップメッシュの生成

すべてのメッシュをバッチ処理して1つの大きなメッシュにしました。その後、そのメッシュがUnityによりアンラップされて、ライトマップUVが生成されるようにしました。このメッシュを使ってライトマップを生成した後、ライトマップUVをこのメッシュからLODメッシュにコピーすることになります。

ライトマップのベイク

ライトマップは、Unityのビルトインライトマップ処理ルーチンを使ってベイクします。

LODの生成

このシステムでは、メッシュを複数のグリッドセルにグループ化することによって組み合わせます。グリッドセルごとに、そこに含まれる全メッシュのバッチメッシュを作成します。各LODレベルで、直前のレベルの2倍のサイズのセルを作成します。こうしておくと、後にそれらのセルを四分木ベースの階層型LODシステムで使えるようになります。遠方のメッシュの複雑度を下げるため、高いLODレベルではバッチ処理の前に小さいメッシュを削除します。その上で、メッシュをバッチ処理して、そのグリッドセルのLODを作成します。三角形の数をさらに減らすため、地形の下にあるすべての三角形を削除することも選択できます。
生成後、ライトマップメッシュのライトマップUVをLODメッシュにコピーします。この時点では、オリジナルのメッシュから頂点を削除しただけなので、ライトマップメッシュ内のすべての頂点は同じ頂点になります。
次に、LOD0を除くすべてのレベルでバッチメッシュを大幅に削減し、各レベルでより大きな誤差を許容できるようにします。この処理は、頂点位置を変化させる可能性があるため、ライトマップUVのコピー後に実行する必要があります。
LOD meshes
ここで、白い線はセルです。緑の正方形はLOD0のメッシュ、青の正方形はLOD1のメッシュ、赤の正方形はLOD2のメッシュです。各正方形が単一のメッシュとなります。LOD0では、小さなオブジェクト(キャンプファイヤ、材木など)がまだ多数見えています。LOD1では、そうした小さなオブジェクトは削除されていますが、木と建物はまだ見えています。LOD2では、大きなオブジェクト(建物、岩)だけが残っています。LOD2のライトマップには、メッシュで削除した影がまだあります。これは、全LODレベルでライトマップが共有されているためです。

LOD構造の作成

次に、生成されたメッシュのためのツリー構造を作成します。また、ライトマップテクスチャーとそのオフセットおよびスケールを保存します。オリジナルのメッシュのコライダーをコピーして、それらを別個のゲームオブジェクトに添付します。
詳しくは、以下のファイルをご覧ください。

サードパーティライブラリ

この例のLOD生成ルーチンの実装では、Mesh Bakerというアセットストアのパッケージを使いました。テクスチャーアトラスを作成したりメッシュを組み合わせたりするときには、このMesh Bakerを使うことを強くおすすめします。独自に作成する方法や、Simplygonなどのライセンスを取得する方法もあります。

サブレベルの組み合わせ

すべてのサブレベルでLODの生成が完了したら、それらを組み合わせて最終レベルを作成する必要があります。

ライトマッピング

Unityでは、ライトマップ情報がすべてライトマップデータアセットの中に保存されます。このアセットは作成することも変更することもできません。つまり、サブレベルを組み合わせて最終レベルにする際に、ライトマップデータが失われてしまうのです。これを回避するため、すべてのライトマップを組み合わせて1つのテクスチャー配列にします。シェーダーに変更を加えて、デフォルトのライトマップテクスチャーを使う代わりにこのテクスチャー配列からサンプリングされるようにします。また、メッシュ上のライトマップUVにライトマップのオフセットとスケールが含まれるよう変更します。
ライトマップテクスチャー配列は大量のメモリを消費することがあるので注意が必要です。そのため、ライトマップテクスチャー配列は圧縮形式にします。そのためにはまず、ソース側テクスチャーを望ましい形式に圧縮し、次にそれをテクスチャー配列にブリッティングします。
不要なsetpass呼び出しを作らないために重要なのが、ライトマップのオフセット、スケール、およびインデックスを頂点属性として保存することです。

シーン

ストリーミング読み込みに対応するため、各LODメッシュをそれぞれ独自のシーンに入れてください。シーンごとに1つのメッシュが含まれるようにします。メッシュはすでに、その最終位置に対してオフセットになっていなければなりません。シーン自体に変形はないため、メッシュがオフセットにならない場合、すべてのメッシュがレベルの原点でスポーンされます。
各シーンをビルド設定に追加してください。次にLODシステムに対して、ビルド設定中に対応するLODメッシュを含むシーンのインデックスを指定します。

LODシステム

メッシュLODの現在の状態とターゲットとする状態のトラッキングを担当するシステムは、効率的でなければなりません。Questハードウェアに、各メッシュからカメラまでの距離をフレームごとにチェックする機能はありません。LOD状態のトラッキングに時間が費やされると、以降のゲームの時間が奪われることになります。

2つのアプローチ

まず、セルごとにカメラまでの独自の距離を最高LOD(最も粗いLOD2)から最低LOD(完全な詳細のLOD0)に向けて順次計算するというグリッドベースのシステムを使いました。セルのLODレベルをオーバーライドできるのは、高めのLODでカバーされるその領域内のすべてのメッシュが、それより低いLODによって置き換えられてギャップが出現しない場合だけでした。残念ながらこのシステムは低速で、セル数の多い大規模レベルのスケールには対応できませんでした。このような仕組みを使う唯一の理由は、各LODレベルが1つ前のLODレベルのちょうど2倍のサイズになるようにしない場合を想定したためです。例えば、LOD0のセルとLOD1のセルの両方を同じサイズにする一方で、LOD1には簡素化されたメッシュを使い続けることが可能です。
そこで、四分木に基づく階層型アプローチに切り替えることにしました。このアプローチでは、大きな領域をとても効率的にカバーできます。LOD2をセルでレンダリングすることを予定している場合、LOD2の領域に含まれることになるLOD1とLOD0のすべてのセルについては、チェックを完全に省略できます。その場合、ほとんどのセルは一度もチェックされないことになります。階層型構造では、ストリーミングも簡素化されます。遅延ストリーミングシステムは、全LODの読み込みが完了するまで親LODを表示することによって簡単に実装できます。あるいは、メモリ消費が増えることと引き換えに、現在表示中のセルの子セルを常に読み込むこともできます。
Quadtree LOD grid with green LOD0 cells near camera, blue LOD1, and red LOD2 at distance.
カメラが黄色のグリッドセルを占有している場合、LOD構造はこのようになります。緑がLOD0、青がLOD1、そして赤がLOD2です。この場合、20個のLOD2メッシュが読み込まれていますが、見えているのはそのうちの14個だけです。24個のLOD1メッシュが読み込まれており、そのうちの20個が見えています。16個のLOD0メッシュが読み込まれており、そのすべてが見えています。外側の白いセルは八分木構造のノードであり、メッシュがありません。

LOD状態

カメラが別のセルに移動するごとに、ツリーが上から下までトラバースされます。ツリーのレベルごとに、セル内でカメラからの距離を計算します。各レベルのセルサイズは、直前のレベルの半分になります。カメラからの距離が1セル未満の場合、読み込んでもメッシュは表示せず、次のレベルに下りていきます。下りる必要がない場合、またはリーフノードに達した場合は、そのノードに関連するメッシュを読み込んで表示します。読み込み中のノードが、それぞれの親ノードに通知します。
まずノードごとに目標とする状態の計算を試みます。次に、正しい状態の適用を試みます。そうする理由は、すべての子ノードの読み込みが完了するまでは、どの子ノードも表示されないようにするためです。そうしないと、メッシュが出現する可能性があります。ノードは、いずれかの子ノードの読み込みを検知すると、その子ノードを非表示にしたままノード自体を強制表示します。子ノードの読み込みが終わるたびに、親ノードに通知されます。親ノードは子ノードを有効にし、すべての読み込みが完了した時点で自身を無効にします。
カメラがセルの間のエッジ上にある場合にLOD状態が急速に変化することを防ぐため、最後にLOD状態を計算したときの位置から1メートル以上カメラが移動するまではLODシステムの更新を停止します。
詳しくは、以下のファイルをご覧ください。

非同期読み込みと非同期解放

当初はシーンを、必要になった時点で読み込み、不要になった時点で解放していました。しかし読み込むための関数と解放するための関数が同期していないため、適切な予防策を講じないと競合状態が生じかねません。
LODストリーミングシステムの最初のイテレーションでは、2つの大きな問題に直面しました。メッシュが2つの別個のLODで2回表示されることがあるという問題と、高LODレベルでメッシュが機能しなくなることがあるという問題です。1つ目の問題は読み込みが完了する前にサブシーンが解放されることが原因であり、2つ目の問題はシーンが最初の読み込み操作完了前に読み込まれ、解放され、また読み込まれることが原因でした。
注: Unityでは、いったん開始された非同期操作は停止できません。また、読み込みが完了する前にシーンを解放することもできません。
これらの問題はいずれも、読み込み操作と解放操作をキューに入れることによって解決しました。前の操作が完了した場合に限って、新しい操作が開始されます。これで、前述の問題がいずれも解決します。読み込みが完了していないシーンを絶対に解放できなくなるからです。2つ目のケースのショートカットとして、2回目の読み込み操作で解放操作をキャンセルして両方をスキップする方法があります。
作業の大半は別スレッドで非同期実行されますが、非同期操作の開始をメインスレッドで行うとコストがかかることがあります。そのため、作業を複数フレームに分散するシステムを実装しました。

アセットの解放

シーンが追加で読み込まれると、そのシーンで使うすべてのアセットも読み込まれます。しかし、そのシーンを解放しても、アセットは解放されません。そのため、すべてのLODがメモリに入れられるまで、メモリ使用量は増え続けます。
Questのメモリが残り少なくなると処理が遅延し始め、最終的にはフリーズやクラッシュにつながる恐れがあります。原因は、システムによってバックグラウンドで低メモリキラーデーモンが呼び出されることです。
Unityには、このような使われなくなったアセットを解放する2つの手段が用意されています。
  • Resources.UnloadUnusedAssets: 使われなくなったアセットをすべて解放する、非常に時間がかかる操作です。これをゲームプレイで使用することは現実的ではありません。複数のフレーム落ちが発生してしまいます。プレイヤーがテレポートしているときや、読み込み中の画面で使うことができます。
  • Resources.UnloadAsset: ゲームプレイ中にアセットを解放するために使うことができます。負荷の高い処理なので、システムによるロードバランスの調整は必要です(メインスレッドで単一メッシュを解放するためのコストは、約0.3msになります)。
注:Resources.UnloadAssetが有効なのは、解放中のシーンで使われていたメッシュを渡す場合のみです。同じメッシュのコピーを渡しても、そのコピーが解放されるだけです。実行時にメッシュ収集を回避できるよう、1つのLODノードで使う全メッシュのシリアライズを試みましたが、うまく行きませんでした。スクリプトによってシリアライズされるメッシュはメッシュのコピーであり、シーンの使うメッシュではないからです。

デバッグツール

これらは複雑なシステムなので、想定事項のチェックやエラーの調査に役立つデバッグツールや視覚化ツールを構築しておくと良いでしょう。

LODデバッグビュー

表示されるLODを明確に視覚化するため、整数マテリアルパラメーター(LODレベル指標に設定、緑が詳細度最高、赤が最低)に基づいて色を出力するシェーダーを作成しました。実行時に、このシェーダーを使ってLODレベルごとにマテリアルを1つずつ作成し、LODレベル間のトランジションを示すマテリアルを1つ作成します。シーンが読み込まれると、LODデバッグが有効かどうかがチェックされます。有効であれば、そのLODレベルに対応するマテリアルが適用されます。
デバッグビューマテリアルに、マテリアル固有のプロパティはありません。そのため、特定のLODレベルのすべてのメッシュで同じマテリアルを共有できます。ただし、メッシュプロパティは依然として利用可能です。また、ライトマップはグローバルパラメーターなので、ライトマップを使って、デバッグシェーダーのフラットなシェーディングになるはずのところで奥行きを作成することができます。

ベンチマークモード

事前に指定した予測可能な自動モードでゲームを実行する手段を設けておくと便利です。例えば、毎晩自動実行することにより、1日委任実行した後にすべてが予期されたように動作していることを確認できます。
このベンチマークモードのため、まず、一連の通過ポイントからの経路を作成するためのシンプルなツールを作成しました。カメラはその経路に沿って一定の速度で移動します。視線方向のためにベンチマーク結果に差異が生じることがないよう、カメラの方向も固定します。
最初の実験では、ベジエ曲線を使って滑らかな経路を作成しました。これは有効なオプションではあるものの、経路の作成が難しくなるため、通過点の間の直線だけを使うように方針を変更しました。
このツールでは、ちょうど地上2メートルの位置に通過点を自動配置しました。カメラが低過ぎたり、逆に空中の高過ぎる位置にあったりすると、オクルージョンカリングのパフォーマンスに影響することがあるため、この位置取りは重要です。
カメラ位置を経路上に固定することはありません。その代わり、カメラは常に経路上の1メートル先を見るようにし、1メートルの間隔を維持して前進するようにします。こうすることで、鋭いカーブを若干スムーズに移動できるようになります。カメラ視線の方向を設定する際には、Y軸を中心とした回転のみ変更します。Y軸に制限するのは、X軸またはZ軸の回転を変化させるとOVRCameraRigの回転を引き起こし、結果としてカメラが上下逆になったり、フレームごとに前方向が切り替わったりすることがあるからです。

自由飛行カメラ

LODシステムが正しく動作しているかどうかは、フレーム内のすべてのタイルを実際に一括表示することで簡単に確認できますが、これを行うには自由飛行カメラを使う必要があります。
このモードでは、Unityスターターサンプルに付属のSimpleCapsuleWithStickMovementスクリプトに変更を加えて、Y方向の動きを可能にしました。必然的に、剛体にかかる重力も無効にしました。ベンチマークモードの場合と同じように、OVRCameraRigはY軸中心以外には回転しないようにします。

LODレベルの強制適用

LODシステムはカメラが近づくにつれて詳細度が上がるように設定されていますが、開発中には、時折、詳細度の低いメッシュを詳しく調査したい場合があります。円滑に調査できるようにするため、LODを特定のレベルにフリーズさせるモードを装備しました。
LODレベルを強制適用する際には、すべてをLOD0に強制適用するとアプリの消費メモリが通常より増加する点を考慮する必要があります。この例ではメモリ制限がないため、すべてのLOD0メッシュを同時に読み込んでも対応できますが、もっと重い環境ではそうすることができないかもしれません。そのような環境では、LODレベル強制適用の対象を、(この例のように)通常表示状態になるセルに限定することを検討してください。LOD2(ここでの最高LODレベル)も表示されないほど遠くにあるセルには、LODを強制適用する必要はありません。これを実現する簡単な方法は、LOD2ノードに達するまで通常どおりツリーをトラバースし、その後はLODレベルを強制適用して、目的のLODレベルに達するまでツリーを下っていくようにすることです。

LODレベルのフリーズ

メッシュのLODレベルは、LODシステムの更新を停止するだけでフリーズできます。
詳しくは、以下のファイルをご覧ください。

LODシステムの利用

LODシステムは、変更や改良をせずにそのまま使用できます。ただし、LOD生成ルーチンについては、コードのいくつかの部分を削除する必要があったため、そのまま使うことはできません。LODシステムを使うには、LODメッシュを独自に生成する必要があります。詳しくは、前出のサードパーティライブラリのセクションをご覧ください。
LODはグリッドパターンに従う必要があり、LODレベルが上がるごとに、1つ前のLODレベルの2倍のサイズになります。これは、LODシステム内部で四分木を使っているためです。LODマネージャのデータを設定するには、LODManager.SetLODを呼び出します。そうすることで、渡されたオブジェクトから四分木が作成されます。この時点では、LODはストリーミングを使わず、すべてのメッシュが読み込まれます(メッシュのオン/オフを切り替えられるだけ)。
メッシュストリーミングを有効にするには、シーンの中にサブレベルコンバイナースクリプトを配置して、実行します。これにより、メッシュごとにシーンが作成され、ストリーミングを使うようにLODマネージャが設定されるはずです。これらのシステムでは、LODのマテリアル、ライトマッピング、競合に関する設定についてさまざまな想定が行われます。このような想定は、実際の設定で機能するように変更する必要があるかもしれません。

オクルージョンカリング

ゲームによっては、オクルージョンカリングによりパフォーマンスに大きな違いが出ることがあります。この例の場合、巨大なビルやそびえ立つ岩場の間の地面を歩きます。これらは、オクルージョンカリングにうってつけのシチュエーションです。

ビルトインオクルージョンカリングシステム

Unityのビルトインオクルージョンカリングシステムは使いやすく、オクルージョンデータの生成に長時間を要さない上に、精度の面でも良好です。ただし、モバイルデバイスにはあまりフィットしません。このシステムでは、オクルージョンデータのサイズに関係なくメモリ消費量が大きくなるようです。CPUに対する負荷や、メインスレッドの消費時間も大きくなることがあります。
そのようなデメリットがあるものの、ここではビルトインオクルージョンシステムを使いました。メモリやCPUがボトルネックにならなかったからです。シーンのオクルージョンデータを生成するには、まずすべてのLODメッシュを(さらにLODメッシュを含むシーンを追加として)読み込む必要があります。その後、通常の方法でオクルージョンデータを生成できます。

カスタムオクルージョンカリングシステム

より複雑なゲームでは、前述の理由によりビルトインオクルージョシステムは使えません。その場合、モバイルデバイスによりフィットする独自のオクルージョンカリングシステムを作成する方法がいくつかあります。
静的ジオメトリの場合、そのようなシステムは比較的簡単に実装できます。その際にはシーンを複数のセルに分割する必要があります。セルごとに、そのメッシュの固有識別情報を出力するカスタムマテリアルを使ってキューブマップに対してシーンをレンダリングします。次に、CPUでそのキューブマップを再度読み込み、表示状態のすべてのメッシュの識別情報を収集します。結果を保管して、実行時にアクセスできるようにします。実行時には、セルに入るたびに、リストにないメッシュを無効にします。実行時にはオクルージョンチェックは実行されないため、これはCPUにとってとても有効なアプローチです。
ここでは、とても簡単に説明しています。ネイティブ実装では、ビルトインオクルージョンカリングシステムを使うだけの場合に比べて、パフォーマンスがはるかに低下する可能性があります。また、レベルのサイズや実装方法によっては、オクルージョンデータのベイク時間も問題になることがあります。
ナビゲーションロゴ
日本語
© 2026 Meta