ボクセル/マインクラフトタイプのゲームのレンダリング速度を改善するにはどうすればよいですか?


35

Minecraftの独自のクローンを作成しています(Javaで作成されています)。今はうまく機能します。40メートルの視聴距離で、MacBook Pro 8,1で60 FPSを簡単に達成できます。(Intel i5 + Intel HD Graphics 3000)。しかし、視距離を70メートルにすると、15〜25 FPSにしか達しません。実際のMinecraftでは、問題なく遠方(= 256m)に表示距離を置くことができます。だから私の質問は、ゲームをより良くするために何をすべきですか?

私が実装した最適化:

  • ローカルチャンクのみをメモリに保持します(プレーヤーの視聴距離に応じて)
  • 錐台カリング(最初にチャンクで、次にブロックで)
  • ブロックの実際に見える面のみを描く
  • 可視ブロックを含むチャンクごとのリストを使用します。表示されるチャンクは、このリストに追加されます。それらが非表示になると、このリストから自動的に削除されます。ブロックは、隣接するブロックを構築または破壊することにより、見えなくなります。
  • 更新ブロックを含むチャンクごとのリストを使用します。表示可能なブロックリストと同じメカニズム。
  • newゲームループ内ではステートメントをほとんど使用しません。(私のゲームは、ガベージコレクターが呼び出されるまで約20秒実行されます)
  • 現在、OpenGL呼び出しリストを使用しています。(glNewList()glEndList()glCallList())ブロックの種類の各側に。

現在、私は照明システムを使用していません。私はすでにVBOについて聞いたことがあります。しかし、私はそれが何であるかを正確に知りません。ただし、それらについてはいくつか調査します。パフォーマンスが向上しますか?VBOを実装する前にglCallLists()、呼び出しリストのリストを使用して渡したいと思います。代わりに千回使用しglCallList()ます。(実際のMineCraftはVBOを使用していないと思うので、これを試してみたいと思います。正しいですか?)

パフォーマンスを改善する他のトリックはありますか?

VisualVMプロファイリングはこれを私に示しました(わずか70フレームのプロファイリング、70メートルの視聴距離):

ここに画像の説明を入力してください

40メートル(246フレーム)のプロファイリング:

ここに画像の説明を入力してください

注:別のスレッドでチャンクを生成しているため、多くのメソッドとコードブロックを同期しています。ゲームループでこれを行う場合、オブジェクトのロックを取得することはパフォーマンスの問題だと思います(もちろん、ゲームループのみがあり、新しいチャンクが生成されない時間について話します)。これは正解?

編集:いくつかのsynchronisedブロックといくつかの他の小さな改善を削除した後。パフォーマンスはすでにはるかに優れています。70メートルの新しいプロファイリング結果は次のとおりです。

ここに画像の説明を入力してください

それがselectVisibleBlocksここの問題であることはかなり明らかだと思います。

前もって感謝します!
マルティン

更新:いくつかの追加の改善(for eachの代わりにforループを使用する、ループ外で変数をバッファリングするなど)後、60の距離を表示できるようになりました。

できるだけ早くVBOを実装するつもりだと思います。

PS:すべてのソースコードはGitHubで入手できます:https :
//github.com/mcourteaux/CraftMania


2
40mでのプロファイルショットを提供して、他のものよりも速くスケールアップする可能性のあるものを確認できますか?
ジェームズ

あまりにも指定されているかもしれませんが、3Dゲームを高速化する方法のテクニックを質問しているだけで、面白そうです。しかし、タイトルはPPLを怖がらせるかもしれません。
グスタボマシエル

@Gtoknu:タイトルとして何を提案しますか?
マーティンコートー

5
あなたが誰に尋ねるかにもよるが、Minecraftは実際にはそれほど速くないと言う人もいるだろう。
-thedaian

「どのテクニックが3Dゲームを高速化できるか」のようなものはもっと良くなるはずだと思います。何かを考えますが、「ベスト」という言葉を使用したり、他のゲームと比較したりしないでください。一部のゲームで何を使用しているのか正確にはわかりません。
グスタボ

回答:


15

個々のブロックで錐台カリングを行うことに言及していますが、それを捨ててみてください。ほとんどのレンダリングチャンクは、完全に表示されるか、まったく表示されない必要があります。

ブロックが与えられたチャンクに変更されたときのMinecraftは、(私はそれを使用するかわからない)ディスプレイリスト/頂点バッファを再構築のみ、および私は行います。ビューが変更されるたびに表示リストを変更する場合、表示リストの利点は得られません。

また、世界の高さのチャンクを使用しているように見えます。Minecraftは、ロードおよび保存とは異なり、表示リストに立方体の 16×16×16チャンクを使用することに注意してください。そうすれば、個々のチャンクをfru頭する理由はさらに少なくなります。

(注:私はMinecraftのコードを調べていません。この情報はすべて、伝聞であるか、プレイ中にMinecraftのレンダリングを観察して得た私の結論です。)


より一般的なアドバイス:

レンダリングは、CPUとGPUの2つのプロセッサで実行されることに注意してください。フレームレートが不十分な場合、どちらか一方が制限リソースになります。プログラムはCPUバウンドまたはGPU バウンドのいずれかです(スワッピングまたはスケジューリングの問題がないと仮定)。

プログラムが100%CPUで実行されている場合(そして、完了するために他に制限のないタスクがない場合)、CPUは非常に多くの作業を行っています。GPUにより多くの処理をさせることと引き換えに、タスクを単純化する(例えば、カリングを少なくする)ことを試みる必要があります。あなたの説明を考えると、これがあなたの問題だと強く思う。

一方、GPUが制限である場合(残念ながら、通常0%-100%の負荷モニターは便利ではありません)、より少ないデータを送信する方法、またはより少ないピクセルを充填する方法を検討する必要があります。


2
素晴らしい参考文献、あなたのウィキで言及されているあなたの研究は私にとって非常に役に立ちました!+1
グスタボマシエル

@OP:見える面のみをレンダリングします(ブロックはレンダリングしません)。病的だが単調な16x16x16チャンクには約800個の可視面があり、含まれるブロックには24,000個の可視面があります。それを行ったら、Kevinの答えには、次に重要な改善点が含まれています。
アンドリュース14

@KevinReidパフォーマンスのデバッグに役立つプログラムがいくつかあります。たとえば、AMD GPU PerfStudioは、CPUまたはGPUがバインドされているかどうか、GPU上でどのコンポーネントがバインドされているか(テクスチャーvsフラグメントvs頂点など)を示します。
akaltar

3

何がVec3f.setをそんなに呼んでいるのですか?各フレームを最初からレンダリングしたいものを構築している場合、それは間違いなくあなたがそれをスピードアップし始めたい場所です。私はあまりOpenGLユーザーではなく、Minecraftがどのようにレンダリングするかについてはあまり知りませんが、あなたが使用している数学関数は今あなたを殺しているようです(あなたがそれらに費やす時間と回数を見てください彼らは呼ばれます-彼らを呼ぶ千カットによる死)。

理想的には、ワールドをセグメント化して、複数のものをグループ化して一緒にレンダリングし、頂点バッファーオブジェクトを構築して、複数のフレームでそれらを再利用できるようにします。VBOを表す世界が何らかの形で(ユーザーが編集するなど)変化する場合にのみ、VBOを変更する必要があります。その後、メモリ消費量を抑えるために、可視範囲に入ると、表現しているVBOを作成/破棄できます。すべてのフレームではなく、VBOが作成されたときにヒットするだけです。

プロファイル内の「呼び出し」カウントが正しい場合、非常に多くのことを非常に多くの回数呼び出しています。(Vec3f.setへの1,000万回の呼び出し...痛い!)


私はこの方法をたくさんのことに使用します。ベクトルの3つの値を設定するだけです。これは、新しいオブジェクトを毎回割り当てるよりもはるかに優れています。
マーティンコートー

2

ここでの私の説明(私自身の実験から)は適用可能です:

ボクセルレンダリングの場合、より効率的なのは、事前に作成されたVBOまたはジオメトリシェーダーですか?

Minecraftとコードは、おそらく固定機能パイプラインを使用します。私自身の努力はGLSLにありましたが、その要旨は一般的に当てはまります。

(メモリから)画面の錐台よりも半ブロック大きい錐台を作成しました。次に、各チャンクの中心点をテストしました(Minecraftには16 * 16 * 128ブロックがあります)。

それぞれの面は、要素配列VBO(チャンクからの多くの面が「フル」になるまで同じVBOを共有します。考えてみましょうmalloc。可能な限り同じVBOで同じテクスチャを持つ面)と、北の頂点インデックス面、南面などは、混合ではなく隣接しています。描画するときglDrawRangeElements、北面に対してa を行います。法線は既に投影および正規化されており、均一です。次に、南向きのフェースなどを行うため、法線はどのVBOにもありません。各チャンクに対して、表示される面のみを放出する必要があります。たとえば、画面の中央にある面だけが左右を描画する必要があります。これはGL_CULL_FACEアプリケーションレベルでは簡単です。

最大のスピードアップであるiircは、各チャンクをポリゴン化するときに内部の面をカリングしました。

また、テクスチャアトラスの管理と、テクスチャによる顔の並べ替え、および同じテクスチャを持つ顔を他のチャンクの顔と同じvboに配置することも重要です。あまりにも多くのテクスチャ変更を避け、テクスチャなどで面を並べ替えることを避けたい場合は、glDrawRangeElements。隣接する同じタイルの面を大きな長方形にマージすることも大したことでした。上記の別の回答でマージについて説明します。

明らかに、これまでに表示されていなかったチャンクのみをポリゴン化します。長時間表示されていないチャンクを破棄し、編集されたチャンクを再ポリゴン化します(これはレンダリングと比較するとまれにしか発生しません)。


あなたの錐台の最適化のアイデアが好きです。しかし、説明で「ブロック」と「チャンク」という用語を混同していませんか?
マーティンコートー

多分そう。ブロックのチャンクは、英語のブロックのブロックです。
ウィル

1

すべての比較(BlockDistanceComparator)はどこから来ていますか?それがソート関数からのものである場合、それを基数ソートで置き換えることができますか(これは漸近的に高速で、比較ベースではありません)?

タイミングを見ると、ソート自体がそれほど悪くない場合でも、relativeToOrigin関数はcompare関数ごとに2回呼び出されています。そのデータはすべて一度計算する必要があります。補助構造をソートする方が速いはずです。例えば

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

その後、擬似コードで

// for i = 0..numBlocks
//     distanceIndexPairs[i].m_distanceSquaredFromOrigin = ...;
///    distanceIndexPairs[i].m_index = i;
// sort distanceIndexPairs
// for i = 0..numBlocks
//    sortedBlock[i] = unsortedBlocks[ distanceIndexPairs.m_index ]

それが有効なJava構造体でない場合は申し訳ありません(私は学部生からJavaに触れていません)が、うまくいけばアイデアが得られます。


これは面白いと思う。Javaには構造体がありません。Javaの世界にはそういうものがありますが、それはデータベースと関係があり、まったく同じことではありません。彼らは公開メンバーと一緒に最終クラスを作成することができます、それはうまくいくと思います。
-Theraot

1

ええ、VBOとCULLフェーシングを使用しますが、それはほとんどすべてのゲームに当てはまります。あなたはそれがプレイヤーに表示されている、場合にのみキューブをレンダリングされてやりたいブロックは、特定の方法で接触している場合は、ブロックやメイクの頂点を追加する(のが、それは地下だからあなたが見ることができないチャンクをしましょう)それはほとんど「大きなブロック」、またはあなたの場合-チャンクのようです。これは貪欲なメッシュ化と呼ばれ、パフォーマンスが大幅に向上します。ゲーム(ボクセルベース)を開発しており、貪欲なメッシュアルゴリズムを使用しています。

このようなすべてをレンダリングする代わりに:

レンダー

次のようにレンダリングします。

render2

これの欠点は、最初のワールドビルドでチャンクごとにさらに計算を行う必要があること、またはプレーヤーがブロックを削除/追加する場合です。

ほぼすべてのタイプのボクセルエンジンが、良好なパフォーマンスのためにこれを必要とします。

ブロック面が別のブロック面に接触しているかどうかを確認し、そうであれば、1つ(またはゼロ)のブロック面としてのみレンダリングします。チャンクを非常に高速にレンダリングしている場合は、コストがかかります。

public void greedyMesh(int p, BlockData[][][] blockData){
        boolean[][][][] mask = new boolean[blockData.length][blockData[0].length][blockData[0][0].length][6];

    for(int side=0; side<6; side++){
        for(int x=0; x<blockData.length; x++){
            for(int y=0; y<blockData[0].length; y++){
                for(int z=0; z<blockData[0][0].length; z++){
                    if(data[x][y][z] > Material.AIR && !mask[x][y][z][side] && blockData[x][y][z].faces[side]){
                        if(side == 0 || side == 1){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=y; i<blockData[0].length; i++){
                                if(i == y){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[x][i][j][side] && blockData[x][i][j].id == blockData[x][y][z].id && blockData[x][i][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[x][i][z+j][side] || blockData[x][i][z+j].id != blockData[x][y][z].id || !blockData[x][i][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x][y+i][z+j][side] = true;
                                }
                            }

                            if(side == 0)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+1, y, z), new VoxelVector3i(x+1, y+height, z+width), new VoxelVector3i(1, 0, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z+width), new VoxelVector3i(x, y+height, z), new VoxelVector3i(-1, 0, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 2 || side == 3){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[i][y][j][side] && blockData[i][y][j].id == blockData[x][y][z].id && blockData[i][y][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y][z+j][side] || blockData[i][y][z+j].id != blockData[x][y][z].id || !blockData[i][y][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y][z+j][side] = true;
                                }
                            }

                            if(side == 2)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y+1, z+width), new VoxelVector3i(x+height, y+1, z), new VoxelVector3i(0, 1, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+width), new VoxelVector3i(x, y, z), new VoxelVector3i(0, -1, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 4 || side == 5){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=y; j<blockData[0].length; j++){
                                        if(!mask[i][j][z][side] && blockData[i][j][z].id == blockData[x][y][z].id && blockData[i][j][z].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y+j][z][side] || blockData[i][y+j][z].id != blockData[x][y][z].id || !blockData[i][y+j][z].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y+j][z][side] = true;
                                }
                            }

                            if(side == 4)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+1), new VoxelVector3i(x, y+width, z+1), new VoxelVector3i(0, 0, 1), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z), new VoxelVector3i(x+height, y+width, z), new VoxelVector3i(0, 0, -1), Material.getColor(data[x][y][z])));
                        }
                    }
                }
            }
        }
    }
}

1
そして、それは価値がありますか?LODシステムの方が適切だと思われます。
マイケルハウス

0

コードはオブジェクトと関数呼び出しにcallsれているように見えます。数字を測定すると、インライン化が発生しているようには見えません。

別のJava環境を見つけようとするか、単純に設定を台無しにすることもできますが、コードを高速でなく単純にする簡単な方法ですが、少なくとも内部でVec3fを停止することはできません。コーディングOOO *。すべてのメソッドを自己完結型にし、いくつかの単純なタスクを実行するためだけに他のメソッドを呼び出さないでください。

編集:場所全体にオーバーヘッドがありますが、レンダリングの前にブロックを並べることはパフォーマンスが最も悪いようです。それは本当に必要ですか?もしそうなら、おそらくループを通過して出発点までの​​各ブロックの距離を計算することから始めて、それでソートする必要があります。

*過度にオブジェクト指向


はい、メモリは節約できますが、CPUは失われます!したがって、OOOはリアルタイムゲームではあまり良くありません。
グスタボマシエル

プロファイリングを開始すると(サンプリングだけでなく)、JVMが通常行うインライン展開はすべて消えます。それは一種の量子論などで、成果をchaningせずに何かを測定することはできませんです:P
マイケル・

@Gtoknuそれは普遍的ではありません。OOOのあるレベルでは、関数呼び出しはインラインコードよりも多くのメモリを占有し始めます。問題のコードには、メモリの損益分岐点付近に相当な部分があると思います。
aaaaaaaaaaaa

0

Math演算をビット単位の演算子に分解することもできます。持っている場合は128 / 16、ビット演算子を作成してみてください:128 << 4。これはあなたの問題に大いに役立ちます。物事をフルスピードで実行しようとしないでください。ゲームを60などのレートで更新し、それを他のもののために分解することもできますが、ボクセルを破壊または配置するか、todoリストを作成する必要があります。これにより、fpsが低下します。エンティティに対して約20の更新レートを実行できます。そして、世界の更新や生成のための10のようなもの。

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