Minecraft風のボクセルの世界を最適化するにはどうすればよいですか?


76

Minecraftの素晴らしい大きな世界は、クアッドコアと肉付きのグラフィックカードを使用しても、ナビゲートが非常に遅いことがわかりました。

Minecraftの遅さは次の原因によるものと思われます。

  • Java。空間分割とメモリ管理がネイティブC ++で高速であるため。
  • 弱い世界分割。

私は両方の仮定で間違っている可能性があります。しかし、これは大きなボクセルの世界を管理する最良の方法について考えさせられました。それは、ブロックは、世界の任意の部分に存在することができる真の3D世界は、であるように、それは大きな3Dアレイ基本的には[x][y][z]世界の各ブロックタイプを有する(すなわちBlockType.Empty = 0BlockType.Dirt = 1等)

このような世界をうまく機能させるには、次のことが必要だと思います。

  • さまざまなツリー(oct / kd / bsp)を使用して、すべてのキューブを分割します。三角形ごとではなくキューブごとにパーティション分割できるため、oct / kdの方が適しているようです。
  • ユーザーに近いブロックが背後のブロックを難読化し、それらをレンダリングすることを無意味にする可能性があるため、いくつかのアルゴリズムを使用して、現在どのブロックが見えるかを判断します。
  • ブロックオブジェクト自体を軽量にしておくと、ツリーにすばやく追加したり削除したりできます。

これに対する正しい答えはないと思いますが、このテーマに関する人々の意見を見てみたいと思います。 大きなボクセルベースの世界でパフォーマンスをどのように改善しますか?



2
それで、あなたは実際に何を求めていますか?大きな世界を管理するための優れたアプローチ、または特定のアプローチに関するフィードバック、または大きな世界を管理することについての意見を求めていますか?
-doppelgreener

1
これまでのところ、これらの種類の最も一般的なアプローチは何かについてです。私が提案したのは、論理的に起こると予想されることだけであるので、私は特にアプローチについてフィードバックした後ではありません。私はこのテーマに関する詳細な情報が本当に欲しいだけで、2、3回検索してもあまり出てこない。私の質問は、レンダリングパフォーマンスだけでなく、そのような大量のデータを管理する方法についてです。たとえば、領域のチャンクなど
-SomeXnaChump

2
投稿に質問を追加して、回答する質問を明確にしてください。;)
doppelgreener

3
「ナビゲートが非常に遅い」とはどういう意味ですか?ゲームが新しい地形を生成するとき、確かに若干の減速がありますが、その後、Minecraftは地形をかなりうまく処理する傾向があります。
テダイアン

回答:


106

ボクセルエンジンロックス

ボクセルエンジングラス

Java対C ++に関しては、両方でボクセルエンジンを記述しました(上記のC ++バージョン)。また、ボクセルエンジンは2004年(流行していなかったとき)から書いています。:)私はC ++のパフォーマンスがはるかに優れていることを少しためらうことなく言うことができます(ただし、コーディングするのはより困難です)。計算速度についてはそれほど重要ではなく、メモリ管理についてはさらに重要です。ボクセルの世界と同じくらいの量のデータを割り当て/割り当て解除するとき、C(++)が勝つ言語です。 しかしながら、あなたの目標について考える必要があります。パフォーマンスが最優先事項である場合は、C ++を使用してください。最先端のパフォーマンスなしでゲームを作成したいだけであれば、Javaは間違いなく受け入れられます(Minecraftで証明されています)。些細なケースやエッジのケースは数多くありますが、一般的に、Javaは(よく書かれた)C ++よりも約1.75〜2.0倍遅いと予測できます。エンジンの最適化が不十分な古いバージョンの動作をここで見ることができます(編集:新しいバージョンはこちら)。チャンクの生成は遅く見えるかもしれませんが、3Dボロノイ図を体積的に生成し、ブルートフォース法でCPUの表面法線、照明、AO、および影を計算していることに留意してください。さまざまな手法を試しましたが、さまざまなキャッシュおよびインスタンス化手法を使用して、約100倍高速のチャンク生成を得ることができます。

残りの質問に答えるために、パフォーマンスを改善するためにできることがたくさんあります。

  1. キャッシング。可能な限り、データを1回計算する必要があります。たとえば、照明をシーンに焼き付けます。ダイナミックライティング(スクリーンスペースで、後処理として)を使用できますが、ライティングをベイク処理すると、三角形の法線を渡す必要がなくなります。つまり、...
  2. ビデオカードに渡すデータはできるだけ少なくします。人々が忘れがちなことの1つは、GPUに渡すデータが多いほど時間がかかることです。単色と頂点位置を渡します。デイ/ナイトサイクルを行いたい場合は、カラーグレーディングを行うか、太陽が徐々に変化するシーンを再計算します。

  3. GPUにデータを渡すのは非常に高価なので、ある点で高速なソフトウェアでエンジンを書くことが可能です。ソフトウェアの利点は、GPUでは不可能なあらゆる種類のデータ操作/メモリアクセスを実行できることです。

  4. バッチサイズで再生します。GPUを使用している場合、渡す各頂点配列の大きさによってパフォーマンスが劇的に変化する可能性があります。したがって、チャンクのサイズを試してください(チャンクを使用する場合)。64x64x64チャンクがかなりうまく機能することがわかりました。何であれ、チャンクを立方体にしてください(直角プリズムはありません)。これにより、コーディングやさまざまな操作(変換など)が簡単になり、場合によってはパフォーマンスが向上します。すべての次元の長さに対して1つの値のみを保存する場合は、計算中にスワップされるレジスタが2つ少ないことに注意してください。

  5. ディスプレイリストを検討してください(OpenGLの場合)。それらは「古い」方法ですが、より高速にすることができます。表示リストを変数にベイクする必要があります...表示リスト作成操作をリアルタイムで呼び出すと、非常に遅くなります。表示リストはどのように高速ですか?頂点ごとの属性に対して、状態のみを更新します。これは、最大6つの面を渡し、次に1つの色(ボクセルの各頂点の色)を渡すことができることを意味します。GL_QUADSとキュービックボクセルを使用している場合、ボクセルあたり最大20バイト(160ビット)節約できます!(アルファなしの15バイト。ただし、通常は4バイトに揃えておく必要があります。)

  6. 私は一般的な手法である「チャンク」またはデータのページをレンダリングするブルートフォース方式を使用します。octreesとは異なり、データの読み取り/処理ははるかに簡単/高速ですが、メモリフレンドリーではありませんが(最近では64ギガバイトのメモリを200〜300ドルで取得できます)...平均的なユーザーが持っているわけではありません。明らかに、1つの巨大な配列を全世界に割り当てることはできません(ボクセルごとに32ビットintを使用すると仮定すると、ボクセルの1024x1024x1024セットは4ギガバイトのメモリです)。したがって、ビューアへの近接度に基づいて、多くの小さな配列を割り当て/割り当て解除します。データを割り当て、必要な表示リストを取得してから、データをダンプしてメモリを節約することもできます。理想的なコンボは、八分木と配列のハイブリッドアプローチを使用することだと思います-世界の手続き型生成、照明などを行うときに配列にデータを保存します

  7. 遠近にレンダリング...クリップされたピクセルが時間を節約します。深度バッファテストに合格しない場合、gpuはピクセルを投げます。

  8. ビューポートでチャンク/ページのみをレンダリングします(自明)。GPUがビューポートの外でポリゴンをクリップする方法を知っていても、このデータを渡すには時間がかかります。このための最も効率的な構造がわからない(「恥ずかしがって」、BSPツリーを書いたことがない)が、チャンクごとの単純なレイキャストでもパフォーマンスが向上する可能性があり、明らかに視錐台に対するテストは時間を節約する。

  9. 明らかな情報ですが、初心者向け:サーフェス上にないポリゴンをすべて削除します。つまり、ボクセルが6つの面で構成されている場合、レンダリングされない(別のボクセルに接触している)面を削除します。

  10. プログラミングで行うすべての一般的なルールとして、CACHE LOCALITY!物事をキャッシュローカルに保つことができれば(ほんの少しでも、大きな違いが生じます。これは、データを(同じメモリ領域に)一致させ、頻繁に処理するメモリ領域を切り替えないことを意味します。 、理想的には、スレッドごとに1つのチャンクで動作し、そのメモリをスレッド専用に保持しますこれはCPUキャッシュに適用されるだけではありません。キャッシュ階層は次のように考えてください(最速から最速):ネットワーク(クラウド/データベース/など) ->ハードドライブ(まだ持っていない場合はSSDを入手してください)、ram(まだ持っていない場合はトリプルチャネルまたはより大きなRAMを入手してください)、CPUキャッシュ、レジスター。後者の終わり、必要以上に交換しないでください。

  11. スレッド。やれ。ボクセルワールドはスレッド化に適しています。各部分は(ほとんど)他の部分とは独立して計算できるからです...スレッド化のためのルーチン。

  12. char / byteデータ型を使用しないでください。またはショートパンツ。あなたの平均的な消費者は、最新のAMDまたはIntelプロセッサを持っているでしょう(おそらくそうでしょう)。これらのプロセッサには8ビットのレジスタはありません。バイトを計算するには、それらを32ビットスロットに入れてから、メモリ内で(おそらく)変換します。コンパイラはあらゆる種類のブードゥーを行うことができますが、32ビットまたは64ビットの数値を使用すると、最も予測可能な(そして最も速い)結果が得られます。同様に、「bool」値は1ビットを取りません。多くの場合、コンパイラはブールにフル32ビットを使用します。データに対して特定のタイプの圧縮を行うのは魅力的かもしれません。たとえば、8個のボクセルがすべて同じタイプ/色の場合、単一の数値(2 ^ 8 = 256の組み合わせ)として保存できます。ただし、これの影響について考える必要があります-それはかなりのメモリを節約するかもしれません、しかし、それは小さな減圧時間でもパフォーマンスを妨げる可能性があります。なぜなら、そのわずかな余分な時間でさえ、あなたの世界のサイズに応じて三次的にスケーリングするからです。レイキャストの計算を想像してください。レイキャストのすべてのステップで、減圧アルゴリズムを実行する必要があります(1つのレイステップで8ボクセルの計算をスマートに一般化する方法を考え出さない限り)。

  13. Jose Chavezが述べているように、フライウェイトデザインパターンは有用です。ビットマップを使用して2Dゲームのタイルを表すのと同じように、いくつかの3Dタイル(またはブロック)タイプから世界を構築できます。これの欠点はテクスチャの繰り返しですが、一緒に収まる分散テクスチャを使用することでこれを改善できます。経験則として、できる限りインスタンス化を利用したいと思います。

  14. ジオメトリを出力するとき、シェーダーで頂点とピクセルの処理を避けます。ボクセルエンジンでは、必然的に多くの三角形が存在するため、単純なピクセルシェーダーでもレンダリング時間を大幅に短縮できます。バッファーにレンダリングする方が良いので、後処理としてピクセルシェーダーを行います。それができない場合は、頂点シェーダーで計算を試してください。可能であれば、他の計算を頂点データにベイクする必要があります。すべてのジオメトリを再レンダリングする必要がある場合(シャドウマッピングや環境マッピングなど)、追加のパスは非常に高価になります。ダイナミックなシーンをあきらめて、より豊かなディテールを優先した方が良い場合もあります。ゲームに変更可能なシーン(すなわち、破壊可能な地形)がある場合、物が破壊されるときにシーンをいつでも再計算できます。再コンパイルは高価ではなく、1秒未満で完了します。

  15. ループを解き、配列をフラットに保ちます!これをしないでください:

    for (i = 0; i < chunkLength; i++) {
     for (j = 0; j < chunkLength; j++) {
      for (k = 0; k < chunkLength; k++) {
       MyData[i][j][k] = newVal;
      }
     }
    }
    //Instead, do this:
    for (i = 0; i < chunkLengthCubed; i++) {
     //figure out x, y, z index of chunk using modulus and div operators on i
     //myData should have chunkLengthCubed number of indices, obviously
     myData[i] = newVal;
    }
    

    編集:より広範なテストを通じて、私はこれが間違っていることがわかった。シナリオに最適なケースを使用してください。一般的に、配列はフラットにする必要がありますが、場合によってはマルチインデックスループを使用する方が高速になることがあります。

編集2:マルチインデックスループを使用する場合、逆方向ではなく、z、y、xの順序でループするのが最善です。あなたのコンパイラはこれを最適化するかもしれませんが、もしそうなら驚くでしょう。これにより、メモリアクセスと局所性の効率が最大化されます。

for (k < 0; k < volumePitch; k++) {
    for (j = 0; j < volumePitch; j++) {
        for (i = 0; i < volumePitch; i++) {
            myIndex = k*volumePitch*volumePitch + j*volumePitch + i;
        }
    }
}
  1. 時には、仮定、一般化、犠牲を払わなければなりません。最良の方法は、世界のほとんどが完全に静的であり、数千フレームごとにしか変化しないと想定することです。世界のアニメーション部分については、それらを個別のパスで行うことができます。また、世界のほとんどが完全に不透明であると仮定します。透明なオブジェクトは、別のパスでレンダリングできます。テクスチャはxユニットごとにのみ変化する、またはオブジェクトはx単位でしか配置できないと仮定します。固定された世界のサイズを想定します...無限の世界が魅力的であるように、予測できないシステム要件につながる可能性があります。たとえば、上記の岩石でのボロノイパターンの生成を単純化するために、すべてのボロノイ中心点は、わずかなオフセット(つまり、暗黙の幾何学的ハッシュ)で均一なグリッドにあると仮定しました。ラップしない(エッジを持つ)世界を想定します。これにより、ラッピング座標系によってもたらされる多くの複雑さを、ユーザーエクスペリエンスに対する最小限のコストで簡素化できます。

私のサイト私の実装の詳細を読むことができます


9
+1。エッセイを読むためのインセンティブとして上部の写真を含む素敵なタッチ。エッセイを読んだ今、それらは必要ではなく、それだけの価値があったと言える。;)
ジョージ・ダケット

ありがとう-彼らが言うように、写真は千の言葉の価値があります。:)私のテキストの壁の威圧感を軽減するだけでなく、説明したテクニックを使用して合理的な速度でレンダリングできるボクセルの数を読者に考えたいと思いました。
ギャバンウールリー

14
SEが特定の回答をお気に入りに追加できるようにしたいのです。
joltmode

2
@PatrickMoriarty#15は非常に一般的なトリックです。コンパイラがこの最適化を行わないと仮定します(ループを展開する可能性がありますが、多次元配列を圧縮することはおそらくないでしょう)。キャッシュのために、すべてのデータを同じ連続したメモリスペースに保持する必要があります。多次元配列は、ポインターの配列であるため、多くのスペースに(潜在的に)割り当てることができます。ループの展開については、コンパイルされたコードがどのように見えるかを考えてください。レジスタとキャッシュのスワップを最小限に抑えるために、生成する変数/命令を最小限に抑える必要があります。どちらをより多くにコンパイルすると思いますか?
ギャバンウールリー

2
ここでのポイントのいくつかは、特にキャッシュ、スレッド化、GPU転送の最小化に関して優れていますが、いくつかは非常に不正確です。5:常に表示リストの代わりにVBO / VAOを使用します。6:より多くのRAMには、より多くの帯域幅が必要です。12のリードで:正確な反対は最新のメモリに当てはまります。保存されるバイトごとに、より多くのデータをキャッシュに収める可能性が高くなります。14:Minecraftでは、ピクセル(それらのすべての遠方の立方体)よりも頂点が多いため、計算をピクセルシェーダーに移動します。

7

Minecraftがより効率的にできることはたくさんあります。たとえば、Minecraftは約16x16タイルの垂直柱全体をロードしてレンダリングします。多くのタイルを不必要に送信してレンダリングするのは非常に効率が悪いと感じています。しかし、言語の選択が重要だとは思いません。

Javaは非常に高速ですが、このデータ指向の場合、C ++には大きな利点があり、配列へのアクセスとバイト内での作業のオーバーヘッドが大幅に削減されます。一方、Javaのすべてのプラットフォームでスレッド化を実行する方がはるかに簡単です。OpenMPまたはOpenCLを利用する予定がない限り、C ++にはその便利さはありません。

私の理想的なシステムは、少し複雑な階層になります。

タイルは単一のユニットであり、マテリアルタイプや照明などの情報を保持するために、おそらく4バイト程度です。

セグメントは、タイルの32x32x32ブロックになります。

  1. その辺全体がブロックである場合、6つの辺のそれぞれにフラグが設定されます。これにより、レンダラーはそのセグメントの背後にあるセグメントをオクルードできます。現在、Minecraftはオクルージョンテストを実行していないようです。しかし、ハードウェアオクルージョンカリングを使用できるという言及がありました。これは、コストは高いものの、ローエンドカードで大量のポリゴンをレンダリングするよりも優れています。
  2. セグメントは、アクティビティ中にのみメモリに読み込まれます(プレイヤー、NPC、水物理学、樹木の成長など)。それ以外の場合は、ディスクからクライアントに圧縮されたまま直接送信されます。

セクターは、セグメントの16x16x8ブロックです。

  1. セクターは各垂直列の最も高いセグメントを追跡するため、それよりも高いセグメントはすぐに空になります。
  2. また、下部のオクルードセグメントを追跡するため、サーフェスからレンダリングする必要があるすべてのセグメントをすばやく取得できます。
  3. セクターは、各セグメントを更新する必要がある次の時間も追跡します(水物理学、樹木の成長など)。このように、各セクターでのロードは、世界を存続させ、タスクを完了するのに十分な長さのセグメントでのみロードするのに十分です。
  4. すべてのエンティティの位置は、セクターに関連して追跡されます。これにより、マップの中心から非常に遠くに移動するときにMinecraftに存在する浮動小数点エラーが防止されます。

世界はセクターの無限の地図です。

  1. 世界は、セクターとその次の更新を管理する責任があります。
  2. 世界は、セグメントを潜在的なパスに沿って先制的にプレイヤーに送信します。Minecraftは、クライアントが要求するセグメントを事後的に送信し、遅延を引き起こします。

私は一般的にこの考えが好きですが、内部的に世界のセクターをどのようにマップしますか?
-Clashsoft

アレイは、セグメント内のタイルおよびセクター内のセグメントにとって最適なソリューションですが、ワールド内のセクターでは、マップサイズを無限にするには別のものが必要になります。私の提案は、ハッシュにXY座標を使用して、ハッシュテーブル(疑似辞書<Vector2i、セクター>)を使用することです。その後、世界は、指定された座標でセクターを単純に検索できます。
ジョシュブラウン

6

Minecraftは、2コアでもかなり高速です。Javaはここでは制限要因ではないようですが、サーバーの遅延が少しあります。地元のゲームのほうがうまくいくように見えるので、そこにいくつかの非効率性を想定します。

あなたの質問については、Notch(Minecraftの著者)が技術についてある程度の長さでブログを書いています。特に、ワ​​ールドは「チャンク」に保存されます(特に、ワールドがまだ入力されていないために欠落している場合に表示されることがあります)。したがって、最初の最適化は、チャンクを表示できるかどうかを決定することです。

ご想像のとおり、チャンク内では、ブロックが表示されるかどうかを、他のブロックによって隠されているかどうかに基づいてアプリが決定する必要があります。

また、ブロックFACESがあることに注意してください。ブロックFACESは、隠れている(つまり、別のブロックが顔を覆っている)ため、またはカメラが向いている方向(カメラが北を向いている場合、ブロックの北面が見えない!)

一般的な手法には、個別のブロックオブジェクトを保持するのではなく、ブロックタイプの「チャンク」、各ブロックに1つのプロトタイプブロック、およびこのブロックのカスタム方法を説明する最小限のデータセットも含まれます。たとえば、カスタムの花崗岩ブロックはありませんが(知っている)、水には各側面に沿ってどれだけ深いかを示すデータがあり、そこから流れの方向を計算できます。

レンダリング速度、データサイズ、または何を最適化しようとしているのかどうか、あなたの質問は明確ではありません。そこに明確化が役立つでしょう。


4
「塊」は通常、チャンクと呼ばれます。
マルコ

良いキャッチ(+1); 回答が更新されました。(もともと記憶のために行っていた、正しい言葉を忘れていた。)
オリー

参照する非効率性は「ネットワーク」とも呼ばれ、同じエンドポイントが通信している場合でも、まったく同じように2回機能することはありません。
エドウィンバック

4

ここに、一般的な情報とアドバイスのほんの一部を紹介します。これは、Minecraftの非常に経験豊富なモデラーとして提供できます(少なくとも部分的にガイダンスを提供できます)。

Minecraftの動作が遅い理由には、いくつかの疑わしい低レベルの設計決定に関係するLOTがあります。たとえば、ブロックが位置によって参照されるたびに、ゲームは境界を超えないことを確認するために約7 ifステートメントで座標を検証します。さらに、「チャンク」(ゲームが動作するブロックの16x16x256単位)を取得し、その中のブロックを直接参照する方法はありません。キャッシュルックアップを回避するためです。私のmodでは、ブロックの配列を直接取得して変更する方法を作成しました。これにより、巨大なダンジョンの生成が、再生不可能な遅延から非常に高速になりました。

編集:異なるスコープで変数を宣言するとパフォーマンスが向上するという主張を削除しましたが、実際にはそうではないようです。私はこの結果を私が試していた他の何かと混同したと信じています(具体的には、ダブルに統合することで爆発関連コードのダブルとフロートの間のキャストを削除します...当然、これは大きな影響を及ぼしました!)

また、それは私が多くの時間を費やす領域ではありませんが、Minecraftのパフォーマンスチョークのほとんどはレンダリングの問題です(ゲーム時間の約75%が私のシステムに割り当てられています)。懸念がマルチプレイヤーでより多くのプレイヤーをサポートするかどうかはそれほど気にしません(サーバーは何もレンダリングしません)が、それは誰のマシンでもプレイできる程度に重要です。

そのため、どのような言語を選択する場合でも、実装/低レベルの詳細を非常によく理解するようにしてください。これは、このようなプロジェクトのわずかな詳細でさえ、すべての違いを生む可能性があるためです(C ++での私の例は、ポインタ?」はい

高レベルの設計を難しくするので、私はその答えを本当に嫌いますが、パフォーマンスが懸念される場合、それは痛ましい真実です。これがお役に立てば幸いです!

また、Gavinの答えは、私が繰り返したくなかったいくつかの詳細をカバーしており(そして、もっともっと!彼は明らかに私よりもこの主題についての知識が豊富です)、私は彼の大部分に同意します。プロセッサと短い可変サイズに関する彼のコメントを実験する必要があります。それについて聞いたことがありません。それが真実であることを自分に証明したいと思います!


2

問題は、まず最初にデータをロードする方法を考えることです。必要なときにマップデータをメモリにストリームする場合、レンダリングできるものに自然な制限があります。これは既にレンダリングパフォーマンスのアップグレードです。

このデータで何をするかはあなた次第です。GFXのパフォーマンスについては、クリッピングを使用して、非表示のオブジェクト、小さすぎて表示できないオブジェクトなどをクリップできます。

グラフィックパフォーマンスのテクニックを探しているだけなら、ネット上で山のようなものを見つけることができると確信しています。


1

注目すべき点は、フライウェイトのデザインパターンです。ここでの回答のほとんどは、このデザインパターンを何らかの形で参照していると思います。

Minecraftが各ブロックタイプのメモリを最小化するために使用している正確な方法はわかりませんが、これはゲームで使用する可能性のある方法です。アイデアは、プロトタイプオブジェクトのように、すべてのブロックに関する情報を保持するオブジェクトを1つだけにすることです。唯一の違いは、各ブロックの場所です。

しかし、場所さえも最小化できます。土地のブロックが1つのタイプであることがわかっている場合、その土地の寸法を1つの場所データのセットで1つの巨大なブロックとして保存してみませんか?

明らかに、唯一知る方法は、独自の実装を開始し、パフォーマンスのためにメモリテストを行うことです。それがどうなるか教えてください!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.