OpenGLを使用してC++でゲームを作成しています。
知らない人のために、OpenGL APIを使用して、glGenBuffers
やglCreateShader
などの多くの呼び出しを行います。これらは、作成したばかりの一意の識別子であるGLuint
のタイプを返します。作成されるものはGPUメモリ上に存在します。
GPUメモリが制限されている場合があることを考えると、複数のオブジェクトで使用されるときに同じものを2つ作成したくない場合があります。
たとえば、シェーダー。シェーダープログラムをリンクすると、GLuint
ができます。シェーダーを使い終わったら、glDeleteShader
(またはそれに影響を与える何か)を呼び出す必要があります。
ここで、次のような浅いクラス階層があるとします。
class WorldEntity
{
public:
/* ... */
protected:
ShaderProgram* shader;
/* ... */
};
class CarEntity : public WorldEntity
{
/* ... */
};
class PersonEntity: public WorldEntity
{
/* ... */
};
私が今まで見たどのコードでも、WorldEntity
に格納するために、すべてのコンストラクターにShaderProgram*
を渡す必要があります。 ShaderProgram
は、OpenGLコンテキストの現在のシェーダー状態へのGLuint
のバインディング、およびシェーダーで実行する必要がある他のいくつかの便利なことをカプセル化する私のクラスです。
私がこれで持っている問題は:
WorldEntity
を構築するために必要な多くのパラメーターがあります(メッシュ、シェーダー、多数のテクスチャーなどがあり、それらはすべて共有できるため、ポインターとして渡されると考えてください)WorldEntity
を作成しているものは何でも、それが必要とするShaderProgram
を知る必要がありますEntityManager
のどのインスタンスが異なるエンティティに渡されるかを知っている、何らかのgulpShaderProgram
クラスが必要です。つまり、Manager
が存在するため、クラスは、EntityManager
に必要なShaderProgram
インスタンスとともに自身を登録する必要があります。または、新しいswitch
派生型ごとに更新する必要があるビッグアスWorldEntity
をマネージャーに登録する必要があります。
私が最初に思ったのは、ShaderManager
クラスを作成し、WorldEntity
クラスを参照またはポインタで渡して、ShaderProgram
クラスを作成し、ShaderManager
とShaderManager
が既存のクラスを追跡できるようにすることでした。 ShaderProgram
s。これにより、すでに存在するものを返すか、必要に応じて新しいものを作成できます。
(ShaderProgram
sの実際のソースコードのファイル名のハッシュを介してShaderProgram
sを保存できます)
だから今:
ShaderManager
ではなくShaderProgram
へのポインターを渡しているので、パラメーターはまだたくさんあります。EntityManager
は必要ありません。エンティティ自体がShaderProgram
のどのインスタンスを作成するかを認識し、ShaderManager
が実際のShaderProgram
sを処理します。ShaderManager
が保持しているShaderProgram
を安全に削除できる時期がわかりません。さて、ShaderProgram
を介して内部のGLuint
を削除するglDeleteProgram
クラスに参照カウントを追加しました。ShaderManager
は使用しません。
だから今:
ShaderProgram
を作成できますShaderProgram
sが重複しています。最後に、私は2つの決定のうちの1つを行うようになりました。
ShaderProgram
sを作成するために呼び出されるstatic class
。ファイル名のハッシュに基づいてShaderProgram
sの内部トラックを保持します。つまり、ShaderProgram
sまたはShaderManager
sへのポインターまたは参照を渡す必要がなくなるため、パラメーターが少なくなります。WorldEntities
は、作成するShaderProgram
のインスタンスに関するすべての知識を持っています
この新しいstatic ShaderManager
は次のことを行う必要があります。
ShaderProgram
が使用された回数のカウントを保持し、ShaderProgram
をコピー不可にするORShaderProgram
sはそれらの参照をカウントし、カウントが0
の場合にデストラクタでのみglDeleteProgram
を呼び出します。また、ShaderManager
は、カウント1のShaderProgram
を定期的にチェックして破棄します。このアプローチの欠点は次のとおりです。
問題になる可能性のあるグローバル静的クラスがあります。 OpenGLコンテキストは、glX
関数を呼び出す前に作成する必要があります。したがって、場合によっては、WorldEntity
が作成され、OpenGLコンテキストの作成前にShaderProgram
を作成しようとすると、クラッシュが発生する可能性があります。
これを回避する唯一の方法は、すべてをポインター/参照として渡すか、クエリ可能なグローバルGLContextクラスを用意するか、構築時にコンテキストを作成するクラスにすべてを保持することです。または、チェックできるグローバルブールIsContextCreated
だけかもしれません。しかし、これはどこにでも醜いコードを与えることを心配しています。
私が進化しているのを見ることができるのは:
Engine
クラス。これにより、構築/分解の順序を適切に制御できます。これは、ラッパー上のラッパーのように、エンジンのユーザーとエンジンの間のインターフェースコードの大きな混乱のようですそして
ShaderProgram
sをstatic ShaderManager
から実際にクリアするタイミング数分ごと?すべてのゲームループ? ShaderProgram
が削除された後、新しいWorldEntity
がリクエストした場合に、シェーダーの再コンパイルを適切に処理しています。しかし、もっと良い方法があると確信しています。それが私がここで求めていることです
- より良い方法それが私がここで求めていることです
ネクロマンシーについてお詫びしますが、過去を含め、OpenGLリソースの管理に関する同様の問題に多くのつまずきを目にしました。そして、私が他の人たちに気付いた苦労した問題の多くは、類似のゲームエンティティをレンダリングするために必要なOGLリソースをラップし、場合によっては抽象化し、カプセル化するという誘惑に起因しています。
そして、私が見つけた「より良い方法」(そこでの私の特定の闘争を終わらせた少なくとも1つ)は、逆のことをやっていました。つまり、ゲームエンティティとコンポーネントを設計する際にOGLの低レベルの側面に関心を持たず、Model
が三角形や頂点プリミティブのように格納する必要があるようなアイデアから離れてください。 VBOをラップまたは抽象化するオブジェクトの形式。
レンダリングの懸念とゲームデザインの懸念
たとえば、CPUイメージなどのより単純な管理要件を伴う、GPUテクスチャよりも少し高いレベルの概念があります(GPUテクスチャを作成してバインドする前に、少なくとも一時的に必要です)。レンダリングに関する懸念がない場合、モデルは、モデルのデータを含むファイルに使用するファイル名を示すプロパティを格納するだけで十分な場合があります。より高レベルでより抽象的で、GLSLシェーダーよりもそのマテリアルのプロパティを記述する「マテリアル」コンポーネントを使用できます。
そして、シェーダーやGPUテクスチャ、VAO/VBO、OpenGLコンテキストなどに関係するコードベースの場所は1つだけであり、それがレンダリングシステム。レンダリングシステムは、ゲームシーン内のエンティティをループする場合があります(私の場合は空間インデックスを通過しますが、空間インデックスを使用した錐台カリングなどの最適化を実装する前に、より簡単に理解して簡単なループから始めることができます)。 「マテリアル」や「画像」などの高レベルのコンポーネントとモデルのファイル名を検出します。
そしてその仕事は、GPUに直接関係しないより高レベルのデータを取得し、シーンで検出されたものと何が起こっているかに基づいて、必要なOpenGLリソースをロード/作成/関連付け/バインド/使用/関連付け解除/破棄することです。シーン。そして、シングルマネージャーや静的バージョンの「マネージャー」などを使用するという誘惑を排除します。これで、すべてのOGLリソース管理がコードベースの1つのシステム/オブジェクトに集中化されます(もちろん、カプセル化されてさらにオブジェクトに分解される可能性があります)。レンダラーによってコードを管理しやすくします)。また、有効なOGLコンテキストの外でリソースを破棄しようとするなど、いくつかのトリップポイントを自然に回避します。これは、すべて(リソースの破棄を含む)が現在有効なGLパイプラインで呼び出されたときのコンテキスト。
設計変更の回避
さらに、コストのかかる中心的な設計変更を回避するための多くの呼吸空間を提供します。これは、一部のマテリアルがレンダリングに複数のレンダリングパス(および複数のシェーダー)を必要とすることを後から発見したためです。マテリアルを単一のGPUシェーダーで統合することを望んでいました。その場合、多くのもので使用される中央インターフェイスにコストのかかる設計変更はありません。レンダリングシステムのローカル実装を更新するだけで、上位レベルのマテリアルコンポーネントでスキンプロパティが検出されたときに、これまで予期されていなかったケースが処理されます。
全体的な戦略
そして、それが私が現在使用している全体的な戦略であり、レンダリングの懸念が複雑になるほど、ますます役立つようになります。欠点としては、シェーダーやVBOなどをゲームエンティティに注入するよりも、事前に少し作業が必要です。また、レンダラーを特定のゲームエンジン(またはその抽象化ですが、より高いレベルと引き換えに)に結合します。ゲームのエンティティとコンセプトは、低レベルのレンダリングの懸念から完全に切り離されます)。また、レンダラーは、関連付けられているデータの関連付けを解除して破棄できるように、エンティティが破棄されたときに通知するためのコールバックなどを必要とする場合があります(ここでの参照カウントまたはshared_ptr
共有リソース用ですが、レンダラー内でローカルにのみです)。また、すべての種類のレンダリングデータを一定の時間でエンティティに関連付けたり関連付けを解除したりするための効率的な方法が必要になる場合があります(ECSは、新しいコンポーネントタイプをオンザフライで関連付ける方法を即座にすべてのシステムに提供する傾向がありますECS-どちらの方法でも難しいことではありません)...しかし、逆に、これらすべての種類のことは、レンダラー以外のシステムにも役立つでしょう。
確かに、実際の実装はこれよりもはるかに微妙な違いがあり、エンジンがレンダリング以外の領域で三角形や頂点などを処理したい場合など、これらのことを少し不明瞭にする可能性があります(例:物理学はそのようなデータに衝突検出を行わせる場合があります) )。しかし、人生がはるかに楽になり始めたのは(少なくとも私にとって)、この種の考え方と戦略の逆転を出発点として受け入れることでした。
また、リアルタイムレンダラーの設計は私の経験では非常に困難です。ハードウェア、シェーディング機能、発見された手法への急速な変更により、これまでに設計した(そして再設計し続ける)ことが最も難しいものです。しかし、このアプローチは、GPUリソースがいつすべてレンダリング実装に集中することによってGPUリソースが作成/破棄されるかという当面の懸念を排除します。また、私にとってさらに有益なのは、そうでなければコストがかかり、カスケード設計の変更(流出する可能性がある)をシフトすることです。レンダリングに直接関係しないコード)は、レンダラー自体の実装にのみ適用されます。そして、変更のコストを削減することで、リアルタイムレンダリングと同じように1年または2年ごとに要件が急速に変化するため、莫大な節約になる可能性があります。
シェーディングの例
シェーディングの例に取り組む方法は、自動車や人物のエンティティなどのGLSLシェーダーのようなものには関心がないということです。私は「マテリアル」に関心があります。これは、それがどのような種類のマテリアルであるかを表すプロパティ(スキン、カーペイントなど)を含む非常に軽量なCPUオブジェクトです。私の実際の場合、視覚的な種類の言語を使用してシェーダーをプログラミングするためのアンリアルブループリントに似たDSELを持っているため、少し洗練されていますが、マテリアルはGLSLシェーダーハンドルを格納しません。
ShaderProgramsは参照をカウントし、カウントが0の場合にデストラクタでglDeleteProgramを呼び出し、ShaderManagerはカウント1のShaderProgramを定期的にチェックして破棄します。
これらのリソースをレンダラーの外の「スペースにある」ように格納および管理しているときも、同じようなことをしていました。デストラクタで直接これらのリソースを破壊しようとした最初の素朴な試みが、それらのリソースを有効なGLコンテキスト(および、有効なコンテキストにないときに、スクリプトまたは何かで誤って作成しようとすることもある)ので、作成と破棄を次の場合に延期する必要がありました。私があなたが説明した同様の「マネージャー」デザインにつながる有効な状況にあったことを保証できました。
CPUリソースをその場所に格納し、レンダラーにGPUリソース管理の懸念事項を処理させると、これらの問題はすべてなくなります。私はどこでもOGLシェーダーを破棄することはできませんが、どこでもCPUマテリアルを破棄してshared_ptr
など、自分自身をトラブルに巻き込むことはありません。
静的なShaderManagerから実際にShaderProgramsをクリアするのはいつですか?数分ごと?すべてのゲームループ? ShaderProgramが削除されたが、新しいWorldEntityが要求した場合に、シェーダーの再コンパイルを適切に処理しています。しかし、もっと良い方法があると確信しています。
GPUリソースを効率的に管理し、不要になったときにそれらをオフロードしたい場合は、私の場合でもその懸念は実際にはトリッキーです。私の場合、大規模なシーンを扱うことができ、ゲームではなくVFXで作業します。アーティストは、リアルタイムレンダリング用に最適化されていない特に強力なコンテンツ(エピックテクスチャ、数百万のポリゴンにわたるモデルなど)を持っている可能性があります。
オフスクリーン(表示錐台の外)でのレンダリングを回避するだけでなく、しばらくの間不要になったときにGPUリソースの負荷を軽減する(ユーザーが遠くの何かを遠くから見ていないと言うなど)ことは、パフォーマンスにとって非常に便利です。しばらくの間)。
したがって、私が最も頻繁に使用するソリューションは、一種の「タイムスタンプ」ソリューションですが、ゲームにどのように適用できるかはわかりません。レンダリング用のリソースの使用/バインドを開始すると(例:錐台カリングテストに合格)、現在の時間をそれらと一緒に保存します。次に、これらのリソースがしばらく使用されていないかどうかを定期的にチェックし、使用されていない場合はアンロード/破棄されます(ただし、GPUリソースの生成に使用された元のCPUデータは、これらのコンポーネントを格納する実際のエンティティが破棄されるまで保持されます)またはそれらのコンポーネントがエンティティから削除されるまで)。リソースの数が増加し、より多くのメモリが使用されるようになると、システムはそれらのリソースのアンロード/破棄についてより積極的になります(システムに負荷がかかるにつれて、破棄される前に古い未使用のリソースに許容されるアイドル時間の量が減少します)。
それはあなたのゲームデザインに大きく依存すると思います。より小さなレベル/ゾーンのような、よりセグメント化されたアプローチのゲームがある場合は、そのレベルに必要なすべてのリソースを事前にロードし(そしてフレームレートを安定に保つための最も簡単な時間を見つけることができます)、ユーザーは次のレベルに進みます。一方、そのようにシームレスな大規模なオープンワールドゲームがある場合、これらのリソースをいつ作成および破棄するかを制御するためのより高度な戦略が必要になる可能性があり、それを途切れることなくすべて実行するための大きな課題があるかもしれません。私のVFXドメインでは、ユーザーがその結果としてゲームオーバーすることはないため、フレームレートへのちょっとした問題はそれほど大きな問題ではありません(理由の範囲内でそれらを排除しようとします)。
私の場合、この複雑さはすべてレンダリングシステムに分離されており、実装に役立つクラスとコードを一般化していますが、有効なGLコンテキストとグローバルを使用する誘惑については心配ありません。またはそのようなもの。
ShaderProgram
クラス自体で参照カウントを行う代わりに、std::shared_ptr<>
のようなスマートポインタークラスに委任することをお勧めします。このようにして、各クラスが1つのジョブのみを実行するようにします。
OpenGLリソースを誤って使い果たすのを防ぐために、ShaderProgram
をコピー不可にすることができます(プライベート/削除されたコピーコンストラクターおよびコピー割り当て演算子)。
共有できるShaderProgram
インスタンスの中央リポジトリを保持するには、次のようにSharedShaderProgramFactory
(静的マネージャーと似ていますが、より適切な名前を使用)を使用できます。
class SharedShaderProgramFactory {
private:
std::weak_ptr<ShaderProgram> program_a;
std::shared_ptr<ShaderProgram> get_progam_a()
{
shared_ptr<ShaderProgram> temp = program_a.lock();
if (!temp)
{
// Requested program does not currently exist, so (re-)create it
temp = new ShaderProgramA();
program_a = temp; // Save for future requests
}
return temp;
}
};
ファクトリクラスは、静的クラス、シングルトン、または必要に応じて渡される依存関係として実装できます。