ハンドルやManagerを渡さずにオブジェクトのハンドルを管理するのに最適なデザインパターンはどれですか。


8

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
{
    /* ... */
};

私が今まで見たどのコードでも、すべてのコンストラクターにShaderProgram*渡して、に格納する必要がありWorldEntityます。ShaderProgramは、GLuintOpenGLコンテキストでの現在のシェーダー状態へののバインディングと、シェーダーを使用するために必要な他のいくつかの役立つことをカプセル化する私のクラスです。

私がこれで持っている問題は:

  • を構築するために必要な多くのパラメーターがありますWorldEntity(メッシュ、シェーダー、多数のテクスチャーなどがあると考えてください。これらはすべて共有できるため、ポインターとして渡されます)
  • どのような作成されたWorldEntityかを知る必要性をShaderProgramそれが必要
  • これはおそらく、異なるエンティティに渡すもののインスタンスを知っているある種のgulp EntityManagerクラスを必要としますShaderProgram

つまりManager、クラスが必要なインスタンスEntityManagerとともにに自分自身を登録する必要があるかShaderProgramswitch新しいWorldEntity派生型ごとに更新する必要がある、マネージャの大物が必要なためです。

私が最初に考えたのは作成することでしたShaderManager(私は管理者が不良である、知っている)私はへの参照やポインタで渡すというクラスをWorldEntityので、彼らは何でも作成することができますクラスShaderProgramを経由して、彼らが望むShaderManagerShaderManager、既存のトラックに保つことができShaderProgram、それができるよう、秒すでに存在するものを返すか、必要に応じて新しいものを作成します。

(私ShaderProgramShaderProgramsの実際のソースコードのファイル名のハッシュを介してsを保存できます)

だから今:

  • ShaderManager代わりにShaderProgramにポインタを渡しているので、まだ多くのパラメータがあります
  • 私はは必要ありません。EntityManagerエンティティ自体がどのインスタンスShaderProgramを作成するかを認識しShaderManager、実際ShaderProgramのを処理します。
  • しかし、今ShaderManagerShaderProgram、それが保持しているを安全に削除できる時期がわかりません。

だから今私は自分に参照カウントを追加しましたShaderProgramその内部を削除し、クラスGLuintを経由してglDeleteProgram、私はと離れて行いますShaderManager

だから今:

  • オブジェクトはShaderProgram必要なものは何でも作成できます
  • しかし、ShaderProgram追跡している外部マネージャーがいないため、重複したsがあります。

最後に、私は2つの決定のうちの1つを行うようになりました。

1.静的クラス

A static classを作成するために呼び出されますShaderProgram秒。これShaderProgramは、ファイル名のハッシュに基づいてsの内部トラックを保持します。つまり、ポインタまたはShaderProgramsまたはShaderManagers への参照を渡す必要がなくなるため、パラメータが少なくなります- 作成するWorldEntitiesインスタンスのすべての知識がありますShaderProgram

この新しいstatic ShaderManagerニーズ:

  • a ShaderProgramが使用された回数のカウントを保持し、私がShaderProgramコピー可能にしない、または
  • ShaderProgramsはそれらの参照をカウントしglDeleteProgram、カウントが0ANDであるときにのみデストラクタを呼び出し、かつが1のをShaderManager定期的にチェックしてShaderProgram破棄します。

このアプローチの欠点は次のとおりです。

  1. 問題になる可能性のあるグローバル静的クラスがあります。OpenGLコンテキストは、glX関数を呼び出す前に作成する必要があります。したがって、潜在的に、a WorldEntityが作成され、ShaderProgramOpenGLコンテキストの作成前に作成しようとすると、クラッシュが発生する可能性があります。

    これを回避する唯一の方法は、ポインタ/参照としてすべてを渡すか、クエリ可能なグローバルGLContextクラスを用意するか、構築時にコンテキストを作成するクラスにすべてを保持することです。または、IsContextCreatedチェックできるグローバルなブール値だけかもしれません。しかし、これはどこにでも醜いコードを与えることを心配しています。

    私が進化しているのを見ることができるのは:

    • Engine構築/解体の順序を適切に制御できるように、他のすべてのクラスが内部に隠されている大きなクラス。これは、ラッパー上のラッパーのように、エンジンのユーザーとエンジンの間のインターフェースコードの大きな混乱のようです
    • インスタンスを追跡し、必要に応じて削除する「Manager」クラス全体。これは必要悪かもしれない?

そして

  1. いつ実際にShaderProgramsをクリアするのstatic ShaderManagerですか?数分ごと?すべてのゲームループ?ShaderProgramが削除された後、新しいがWorldEntity要求した場合に、シェーダーの再コンパイルを適切に処理しています。しかし、もっと良い方法があると確信しています。

2.より良い方法

それが私がここで求めていることです


2
「WorldEntityを構築するために必要なパラメーターはたくさんある」と言ったときに頭に浮かぶのは、ある種のファクトリーパターンが配線を処理するために必要なものであることです。さらに、ここでは必ずしも依存性注入が必要だと言っているわけではありませんが、洞察に富むようになる前にそのパスを見下ろしていない場合は、ここで話している「マネージャー」は、ライフタイムスコープハンドラーに似ています。
J Trana 2015年

したがって、WorldEntitys を構築するためにファクトリー・クラスを実装するとします。問題の一部をシフトしていませんか?これは、WorldFactoryクラスが各WolrdEntityに正しいShaderProgramを渡す必要があるためです。
NeomerArcana 2015年

良い質問。しばしば、いいえ-そしてここに理由があります。多くの場合、特定のShaderProgramを用意する必要はありません。インスタンス化するものを変更したい場合や、完全にシミュレーションされたShaderProgramを使用して単体テストを作成したい場合があります。私が尋ねる質問は次のとおりです。そのエンティティにとって、どのシェーダープログラムが本当に重要なのですか?場合によってはそうかもしれませんが、MySpecificShaderProgramポインターではなくShaderProgramポインターを使用しているため、そうでない場合があります。また、ShaderProgramスコープの問題がファクトリレベルに移行し、シングルトン間の変更などを簡単に行えるようになりました。
J Trana '25年

回答:


4
  1. より良い方法それが私がここで求めていることです

ネクロマンシーについてお詫びしますが、過去を含め、OpenGLリソースの管理に関する同様の問題に多くのつまずきを目にしました。そして、私が他の人たちに気付いた苦労の多くは、類似のゲームエンティティをレンダリングするために必要なOGLリソースをラップし、場合によっては抽象化し、カプセル化するという誘惑に起因しています。

そして、私が見つけた「より良い方法」(少なくとも私の特定の闘争を終わらせた方法)は、逆のことをするものでした。つまり、ゲームエンティティとコンポーネントを設計する際にOGLの低レベルの側面に関心を持たずModel、三角形や頂点プリミティブのようにオブジェクトのラッピングやVBOを抽象化します。

レンダリングの問題とゲームデザインの問題

たとえば、CPUイメージなどのより単純な管理要件を伴う、GPUテクスチャよりも少し高いレベルの概念があります(GPUテクスチャを作成してバインドする前に、少なくとも一時的に必要です)。レンダリングに関する懸念がない場合、モデルは、モデルのデータを含むファイルに使用するファイル名を示すプロパティを格納するだけで十分な場合があります。より高レベルで抽象的な「マテリアル」コンポーネントを使用して、GLSLシェーダーよりもそのマテリアルのプロパティを記述できます。

そして、シェーダーやGPUテクスチャ、VAO / VBO、OpenGLコンテキストなどに関係するコードベースの場所は1つだけで、それがレンダリングシステムの実装です。レンダリングシステムは、ゲームシーンのエンティティをループする場合があります(私の場合は空間インデックスを通過しますが、空間インデックスを使用した錐台カリングなどの最適化を実装する前に、より簡単に理解して簡単なループから始めることができます)。 「マテリアル」や「画像」などの高レベルのコンポーネントとモデルのファイル名を検出します。

そしてその仕事は、GPUに直接関係のないより高レベルのデータを取得し、シーンで発見したものと何が起こっているかに基づいて必要なOpenGLリソースをロード/作成/関連付け/バインド/使用/関連付け解除/破棄することですシーン。そして、シングルマネージャーや「マネージャー」の静的バージョンなどを使用するという誘惑を排除します。これで、すべてのOGLリソース管理がコードベースの1つのシステム/オブジェクトに集中化されます(もちろん、カプセル化されたオブジェクトにさらに分解する可能性があります)。レンダラーによってコードを管理しやすくします)。また、有効なOGLコンテキストの外部にあるリソースを破棄しようとするなどのいくつかのトリップポイントを自然に回避します。

設計変更の回避

さらに、コストのかかる中心的な設計変更を回避するための多くの呼吸空間を提供します。これは、一部のマテリアルが、サブサーフェススキャッタリングパスやスキンマテリアルのシェーダーのように、レンダリングするために複数のレンダリングパス(および複数のシェーダー)を必要とすることが後から判明したためです。マテリアルを単一のGPUシェーダーで統合することを望んでいました。その場合、多くのもので使用される中央インターフェイスにコストのかかる設計変更はありません。レンダリングシステムのローカル実装を更新するだけで、高レベルのマテリアルコンポーネントでスキンプロパティが検出されたときに、これまで予期されていなかったこのケースを処理できます。

全体的な戦略

そして、それは私が現在使用している全体的な戦略であり、特にレンダリングの懸念が複雑になるほど、ますます役立ちます。欠点としては、シェーダーやVBOなどをゲームエンティティに注入するよりも、事前に少し作業が必要です。また、レンダラーを特定のゲームエンジン(またはその抽象化ですが、より高いレベルと引き換えに)に結合します。ゲームのエンティティとコンセプトは、低レベルのレンダリングの懸念から完全に切り離されます)。また、レンダラーは、エンティティが破棄されたときに通知するコールバックなどが必要になる場合があります。これにより、関連付けられているデータの関連付けを解除して破棄できるようになります(ここで参照カウントを使用するか、shared_ptr共有リソース用ですが、レンダラー内でローカルにのみです)。また、すべての種類のレンダリングデータを一定の時間でエンティティに関連付けたり関連付けを解除したりするための効率的な方法が必要になる場合があります(ECSは、新しいコンポーネントタイプをオンザフライで関連付ける方法を即座にすべてのシステムに提供する傾向がありますECS-どちらの方法でもそれほど難しくないはずですが)...しかし、逆に言えば、これらすべての種類のことは、レンダラー以外のシステムにも役立つでしょう。

確かに、実際の実装はこれよりもはるかに微妙な違いがあり、エンジンがレンダリング以外の領域で三角形や頂点などを処理したい場合など、これらのことを少し不明瞭にする可能性があります(例:物理学はそのようなデータに衝突検出を行わせる場合があります) )。しかし、人生がはるかに楽になり始めたのは(少なくとも私にとって)、この種の考え方と戦略の逆転を出発点として受け入れることでした。

また、リアルタイムレンダラーの設計は私の経験では非常に困難です。ハードウェア、シェーディング機能、発見された手法への急速な変更により、これまでに設計した(そして再設計し続ける)最も困難なことです。しかし、このアプローチは、GPUリソ​​ースがいつすべてレンダリング実装に集中することによってGPUリソ​​ースが作成/破壊されるかという当面の懸念を排除します。また、私にとってさらに有益なのは、そうでなければコストがかかり、カスケード設計変更(流出する可能性がある)をシフトすることです。レンダリングに直接関係しないコード)は、レンダラー自体の実装にのみ適用されます。そして、変更のコストを削減することで、リアルタイムレンダリングと同じくらい迅速に要件を毎年1、2回変更することで、大幅な節約を実現できます。

シェーディングの例

あなたのシェーディングの例に取り組む方法は、車や人のエンティティなどのGLSLシェーダーのようなものには関心がないということです。私は「マテリアル」に関心があります。これは、それがどのような種類のマテリアルであるかを表すプロパティ(スキン、カーペイントなど)を含む非常に軽量なCPUオブジェクトです。私の実際の場合、視覚的な種類の言語を使用してシェーダーをプログラミングするためのUnreal Blueprintsに似たDSELを持っているので少し洗練されていますが、マテリアルはGLSLシェーダーハンドルを格納しません。

ShaderProgramsはそれらの参照をカウントし、カウントが0で、ShaderManagerが1のカウントでShaderProgramを定期的にチェックして破棄する場合にのみ、デストラクタでglDeleteProgramを呼び出します。

これらのリソースをレンダラーの外側の「空間にある」ように格納および管理しているときも、同じようなことをしていました。なぜなら、デストラクタで直接これらのリソースを破壊しようとした最初の素朴な試みが、それらのリソースを有効なGLコンテキスト(および、有効なコンテキストにないときに、スクリプトまたは何かで誤って作成しようとすることもある)ので、有効なコンテキストにあることを保証できる場合に、作成と破棄を延期する必要がありました。あなたが説明する同様の「マネージャー」デザインにつながります。

CPUリソースをその場所に格納し、レンダラーにGPUリソ​​ース管理の懸念事項を処理させると、これらの問題はすべてなくなります。私はOGLシェーダーをどこでも破壊することはできませんが、CPUマテリアルをどこでも破壊でき、簡単に使用できますshared_ptr

静的なShaderManagerから実際にShaderProgramsをクリアするのはいつですか?数分ごと?すべてのゲームループ?ShaderProgramが削除されたが、新しいWorldEntityが要求した場合に、シェーダーの再コンパイルを適切に処理しています。しかし、もっと良い方法があると確信しています。

GPUリソ​​ースを効率的に管理し、不要になったときにそれらをオフロードしたい場合は、私の場合でもその懸念は実際にはトリッキーです。私の場合、私は大規模なシーンを扱うことができ、ゲームではなくVFXで作業します。アーティストは、リアルタイムレンダリング用に最適化されていない特に強力なコンテンツ(壮大なテクスチャ、何百万ものポリゴンにわたるモデルなど)を持っている可能性があります。

オフスクリーン(表示錐台の外)にあるときのレンダリングを回避するだけでなく、しばらくの間不要になったときにGPUリソ​​ースの負荷を軽減する(ユーザーが離れたところにある何かを見ていないと言う場合)ことは、パフォーマンスにとって非常に便利です。しばらくの間)。

したがって、私が最も頻繁に使用するソリューションは、一種の「タイムスタンプ」ソリューションですが、ゲームにどのように適用できるかはわかりません。レンダリング用のリソースの使用/バインドを開始すると(例:錐台カリングテストに合格)、現在の時間をそれらと一緒に保存します。次に、これらのリソースがしばらく使用されていないかどうかを定期的にチェックし、使用されていない場合はアンロード/破棄されます(ただし、GPUリソ​​ースの生成に使用された元のCPUデータは、これらのコンポーネントを格納する実際のエンティティが破棄されるまで保持されます)またはそれらのコンポーネントがエンティティから削除されるまで)。リソースの数が増加し、より多くのメモリが使用されると、システムはそれらのリソースのアンロード/破棄についてより積極的になります(古いもので許容されるアイドル時間の量

それはあなたのゲームデザインに大きく依存すると思います。より小さなレベル/ゾーンのような、よりセグメント化されたアプローチのゲームがある場合は、そのレベルに必要なすべてのリソースを事前にロードして(フレームレートを安定に保つための最も簡単な時間を見つけることができます)、ユーザーは次のレベルに進みます。一方、そのようにシームレスな大規模なオープンワールドゲームがある場合、これらのリソースをいつ作成および破棄するかを制御するには、より高度な戦略が必要になる可能性があります。私のVFXドメインでは、ユーザーがゲームオーバーすることがないので、フレームレートに少し問題があったとしてもそれほど大きな問題ではありません(私はそれを合理的な範囲内で排除しようとします)。

私の場合、この複雑さはすべてレンダリングシステムに分離されています。実装を支援するためにクラスとコードを一般化しましたが、グローバルなどを使用する有効なGLコンテキストと誘惑については心配ありません。


1

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;
  }
};

ファクトリクラスは、静的クラス、シングルトン、または必要に応じて渡される依存関係として実装できます。


-3

OpenglはCライブラリとして設計されており、手続き型ソフトウェアの特性を備えています。Cライブラリであることから生じるopenglルールの1つは、次のようになります。

「シーンの複雑さが増すと、コードの周りに渡す必要のあるハンドルが増えます」

これは、opengl apiの機能です。基本的に、コード全体がmain()関数内にあり、それらのすべてのハンドルがmain()のローカル変数を介して渡されることを前提としています。

このルールの重要性は次のとおりです。

  1. データ受け渡しパスにタイプまたはインターフェースを置かないでください。理由は、このタイプは不安定で、シーンの複雑さが増すと常に変更が必要になるためです。
  2. データ受け渡しパスはmain()関数内にある必要があります。

なぜこれが票を引き付けているのか知りたいと思うなら。トピックに不慣れな方は、この回答のどこが悪いのかを知っておくと役に立ちます。
RubberDuck、2015

1
私はここで反対票を投じなかったし、好奇心も持ちませんでしたが、おそらくOGLは内部にあるコード全体を中心に設計されているという考えはmain(少なくとも言い回しでは)少し難しいようです。
ドラゴンエネルギー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.