Java対C ++に関しては、両方でボクセルエンジンを記述しました(上記のC ++バージョン)。また、ボクセルエンジンは2004年(流行していなかったとき)から書いています。:)私はC ++のパフォーマンスがはるかに優れていることを少しためらうことなく言うことができます(ただし、コーディングするのはより困難です)。計算速度についてはそれほど重要ではなく、メモリ管理についてはさらに重要です。ボクセルの世界と同じくらいの量のデータを割り当て/割り当て解除するとき、C(++)が勝つ言語です。 しかしながら、あなたの目標について考える必要があります。パフォーマンスが最優先事項である場合は、C ++を使用してください。最先端のパフォーマンスなしでゲームを作成したいだけであれば、Javaは間違いなく受け入れられます(Minecraftで証明されています)。些細なケースやエッジのケースは数多くありますが、一般的に、Javaは(よく書かれた)C ++よりも約1.75〜2.0倍遅いと予測できます。エンジンの最適化が不十分な古いバージョンの動作をここで見ることができます(編集:新しいバージョンはこちら)。チャンクの生成は遅く見えるかもしれませんが、3Dボロノイ図を体積的に生成し、ブルートフォース法でCPUの表面法線、照明、AO、および影を計算していることに留意してください。さまざまな手法を試しましたが、さまざまなキャッシュおよびインスタンス化手法を使用して、約100倍高速のチャンク生成を得ることができます。
- キャッシング。可能な限り、データを1回計算する必要があります。たとえば、照明をシーンに焼き付けます。ダイナミックライティング(スクリーンスペースで、後処理として)を使用できますが、ライティングをベイク処理すると、三角形の法線を渡す必要がなくなります。つまり、...
ビデオカードに渡すデータはできるだけ少なくします。人々が忘れがちなことの1つは、GPUに渡すデータが多いほど時間がかかることです。単色と頂点位置を渡します。デイ/ナイトサイクルを行いたい場合は、カラーグレーディングを行うか、太陽が徐々に変化するシーンを再計算します。
GPUにデータを渡すのは非常に高価なので、ある点で高速なソフトウェアでエンジンを書くことが可能です。ソフトウェアの利点は、GPUでは不可能なあらゆる種類のデータ操作/メモリアクセスを実行できることです。
バッチサイズで再生します。GPUを使用している場合、渡す各頂点配列の大きさによってパフォーマンスが劇的に変化する可能性があります。したがって、チャンクのサイズを試してください(チャンクを使用する場合)。64x64x64チャンクがかなりうまく機能することがわかりました。何であれ、チャンクを立方体にしてください(直角プリズムはありません)。これにより、コーディングやさまざまな操作(変換など)が簡単になり、場合によってはパフォーマンスが向上します。すべての次元の長さに対して1つの値のみを保存する場合は、計算中にスワップされるレジスタが2つ少ないことに注意してください。
ディスプレイリストを検討してください(OpenGLの場合)。それらは「古い」方法ですが、より高速にすることができます。表示リストを変数にベイクする必要があります...表示リスト作成操作をリアルタイムで呼び出すと、非常に遅くなります。表示リストはどのように高速ですか?頂点ごとの属性に対して、状態のみを更新します。これは、最大6つの面を渡し、次に1つの色(ボクセルの各頂点の色)を渡すことができることを意味します。GL_QUADSとキュービックボクセルを使用している場合、ボクセルあたり最大20バイト(160ビット)節約できます!(アルファなしの15バイト。ただし、通常は4バイトに揃えておく必要があります。)
私は一般的な手法である「チャンク」またはデータのページをレンダリングするブルートフォース方式を使用します。octreesとは異なり、データの読み取り/処理ははるかに簡単/高速ですが、メモリフレンドリーではありませんが(最近では64ギガバイトのメモリを200〜300ドルで取得できます)...平均的なユーザーが持っているわけではありません。明らかに、1つの巨大な配列を全世界に割り当てることはできません(ボクセルごとに32ビットintを使用すると仮定すると、ボクセルの1024x1024x1024セットは4ギガバイトのメモリです)。したがって、ビューアへの近接度に基づいて、多くの小さな配列を割り当て/割り当て解除します。データを割り当て、必要な表示リストを取得してから、データをダンプしてメモリを節約することもできます。理想的なコンボは、八分木と配列のハイブリッドアプローチを使用することだと思います-世界の手続き型生成、照明などを行うときに配列にデータを保存します
遠近にレンダリング...クリップされたピクセルが時間を節約します。深度バッファテストに合格しない場合、gpuはピクセルを投げます。
ビューポートでチャンク/ページのみをレンダリングします(自明)。GPUがビューポートの外でポリゴンをクリップする方法を知っていても、このデータを渡すには時間がかかります。このための最も効率的な構造がわからない(「恥ずかしがって」、BSPツリーを書いたことがない)が、チャンクごとの単純なレイキャストでもパフォーマンスが向上する可能性があり、明らかに視錐台に対するテストは時間を節約する。
明らかな情報ですが、初心者向け:サーフェス上にないポリゴンをすべて削除します。つまり、ボクセルが6つの面で構成されている場合、レンダリングされない(別のボクセルに接触している)面を削除します。
プログラミングで行うすべての一般的なルールとして、CACHE LOCALITY!物事をキャッシュローカルに保つことができれば(ほんの少しでも、大きな違いが生じます。これは、データを(同じメモリ領域に)一致させ、頻繁に処理するメモリ領域を切り替えないことを意味します。 、理想的には、スレッドごとに1つのチャンクで動作し、そのメモリをスレッド専用に保持しますこれはCPUキャッシュに適用されるだけではありません。キャッシュ階層は次のように考えてください(最速から最速):ネットワーク(クラウド/データベース/など) ->ハードドライブ(まだ持っていない場合はSSDを入手してください)、ram(まだ持っていない場合はトリプルチャネルまたはより大きなRAMを入手してください)、CPUキャッシュ、レジスター。後者の終わり、必要以上に交換しないでください。
スレッド。やれ。ボクセルワールドはスレッド化に適しています。各部分は(ほとんど)他の部分とは独立して計算できるからです...スレッド化のためのルーチン。
char / byteデータ型を使用しないでください。またはショートパンツ。あなたの平均的な消費者は、最新のAMDまたはIntelプロセッサを持っているでしょう(おそらくそうでしょう)。これらのプロセッサには8ビットのレジスタはありません。バイトを計算するには、それらを32ビットスロットに入れてから、メモリ内で(おそらく)変換します。コンパイラはあらゆる種類のブードゥーを行うことができますが、32ビットまたは64ビットの数値を使用すると、最も予測可能な(そして最も速い)結果が得られます。同様に、「bool」値は1ビットを取りません。多くの場合、コンパイラはブールにフル32ビットを使用します。データに対して特定のタイプの圧縮を行うのは魅力的かもしれません。たとえば、8個のボクセルがすべて同じタイプ/色の場合、単一の数値(2 ^ 8 = 256の組み合わせ)として保存できます。ただし、これの影響について考える必要があります-それはかなりのメモリを節約するかもしれません、しかし、それは小さな減圧時間でもパフォーマンスを妨げる可能性があります。なぜなら、そのわずかな余分な時間でさえ、あなたの世界のサイズに応じて三次的にスケーリングするからです。レイキャストの計算を想像してください。レイキャストのすべてのステップで、減圧アルゴリズムを実行する必要があります(1つのレイステップで8ボクセルの計算をスマートに一般化する方法を考え出さない限り)。
Jose Chavezが述べているように、フライウェイトデザインパターンは有用です。ビットマップを使用して2Dゲームのタイルを表すのと同じように、いくつかの3Dタイル(またはブロック)タイプから世界を構築できます。これの欠点はテクスチャの繰り返しですが、一緒に収まる分散テクスチャを使用することでこれを改善できます。経験則として、できる限りインスタンス化を利用したいと思います。
ジオメトリを出力するとき、シェーダーで頂点とピクセルの処理を避けます。ボクセルエンジンでは、必然的に多くの三角形が存在するため、単純なピクセルシェーダーでもレンダリング時間を大幅に短縮できます。バッファーにレンダリングする方が良いので、後処理としてピクセルシェーダーを行います。それができない場合は、頂点シェーダーで計算を試してください。可能であれば、他の計算を頂点データにベイクする必要があります。すべてのジオメトリを再レンダリングする必要がある場合(シャドウマッピングや環境マッピングなど)、追加のパスは非常に高価になります。ダイナミックなシーンをあきらめて、より豊かなディテールを優先した方が良い場合もあります。ゲームに変更可能なシーン(すなわち、破壊可能な地形)がある場合、物が破壊されるときにシーンをいつでも再計算できます。再コンパイルは高価ではなく、1秒未満で完了します。
ループを解き、配列をフラットに保ちます!これをしないでください:
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;
}
編集:より広範なテストを通じて、私はこれが間違っていることがわかった。シナリオに最適なケースを使用してください。一般的に、配列はフラットにする必要がありますが、場合によってはマルチインデックスループを使用する方が高速になることがあります。