今学期、大学でコンピュータグラフィックスのコースを受講しました。現時点では、ハイトマップ、平均化法線、テッセレーションなど、より高度なものに取り組み始めています。
私はオブジェクト指向のバックグラウンドを持っているので、私たちが行うすべてのことを再利用可能なクラスに入れようとしています。カメラクラスの作成は成功しました。これは、ほとんどがgluLookAt()の1回の呼び出しに依存しているためです。これは、OpenGLステートマシンの他の部分からほとんど独立しています。
しかし、私は他の面でいくつかの問題を抱えています。オブジェクトを使用してプリミティブを表すことは、私にとって実際には成功していません。これは、実際のレンダリング呼び出しが、現在バインドされているテクスチャなど、非常に多くの外部のものに依存しているためです。特定のクラスのサーフェス法線から頂点法線に突然変更したい場合、深刻な頭痛の種になります。
OOの原則がOpenGLコーディングに適用できるかどうか疑問に思い始めています。少なくとも、クラスの粒度を低くする必要があると思います。
これに関するスタックオーバーフローコミュニティの見解は何ですか? OpenGLコーディングのベストプラクティスは何ですか?
最も実用的なアプローチは、直接適用できない(または遅い、ハードウェアアクセラレーションされていない、またはハードウェアに適合しなくなった)OpenGL機能のほとんどを無視することのようです。
OOPであるかどうかにかかわらず、一部のシーンをレンダリングするには、通常、さまざまなタイプとエンティティがあります。
ジオメトリ(メッシュ)。ほとんどの場合、これは頂点の配列とインデックスの配列です(つまり、三角形ごとに3つのインデックス、別名「三角形リスト」)。頂点は任意の形式にすることができます(たとえば、float3位置のみ、float3位置+ float3法線、float3位置+ float3法線+ float2 texcoordなど)。したがって、必要なジオメトリの一部を定義するには、次のようにします。
OOP土地にいる場合、このクラスをMeshと呼ぶことができます。
Materials-ジオメトリの一部がどのようにレンダリングされるかを定義するもの。最も単純なケースでは、これは、たとえば、オブジェクトの色である可能性があります。または、照明を適用する必要があるかどうか。または、オブジェクトをアルファブレンドする必要があるかどうか。または、使用するテクスチャ(またはテクスチャのリスト)。または、使用する頂点/フラグメントシェーダー。など、可能性は無限大です。 必要なものを材料に入れることから始めます。 OOP土地では、そのクラスは(驚きです!)aMaterialと呼ばれる可能性があります。
シーン-ジオメトリの断片、マテリアルのコレクション、シーンの内容を定義する時間があります。単純なケースでは、シーン内の各オブジェクトは次のように定義できます。-使用するジオメトリ(メッシュへのポインタ)、-レンダリング方法(マテリアルへのポインタ)、-オブジェクトの配置場所。これは、4x4変換行列、4x3変換行列、またはベクトル(位置)、クォータニオン(方向)、および別のベクトル(スケール)である可能性があります。これをOOP土地のノードと呼びましょう。
カメラ。ええと、カメラは「配置された場所」(ここでも、4x4または4x3マトリックス、または位置と向き)に加えて、いくつかの投影パラメーター(視野、アスペクト比など)にすぎません。
基本的にはそれだけです!メッシュとマテリアルを参照するノードの束であるシーンがあり、ビューアがどこにあるかを定義するカメラがあります。
さて、実際のOpenGL呼び出しをどこに置くかは、設計上の問題にすぎません。 OpenGL呼び出しをNodeまたはMeshまたはMaterialクラスに入れないでください。代わりに、OpenGLRendererのようなものを作成してください。 -)シーンをトラバースしてすべての呼び出しを発行できるか、さらに良いことに、OpenGLとは無関係にシーンをトラバースするものを作成し、低レベルの呼び出しをOpenGL依存クラスに入れます。
そうです、上記のすべてはプラットフォームにほとんど依存していません。このようにすると、glRotate、glTranslate、gluLookAtなどはまったく役に立たないことがわかります。すでにすべての行列があります。OpenGLに渡すだけです。これは、実際のゲーム/アプリケーションの実際のコードのほとんどがとにかく機能する方法です。
もちろん、上記はより複雑な要件によって複雑になる可能性があります。特に、マテリアルは非常に複雑になる可能性があります。メッシュは通常、多くの異なる頂点フォーマットをサポートする必要があります(効率のためにパックされた法線など)。シーンノードは階層に編成する必要がある場合があります(これは簡単です。ノードに親/子ポインタを追加するだけです)。スキンメッシュとアニメーションは、一般的に複雑さを増します。等々。
しかし、主なアイデアは単純です。ジオメトリがあり、マテリアルがあり、シーン内にオブジェクトがあります。次に、いくつかの小さなコードでそれらをレンダリングできます。
OpenGLの場合、メッシュを設定すると、VBOオブジェクトが作成/アクティブ化/変更される可能性が高くなります。ノードをレンダリングする前に、マトリックスを設定する必要があります。また、マテリアルを設定すると、残りのOpenGL状態のほとんど(ブレンド、テクスチャリング、ライティング、コンバイナー、シェーダーなど)に影響します。
オブジェクト変換
変換を行うためにOpenGLに依存することは避けてください。多くの場合、チュートリアルでは、変換行列スタックの操作方法を説明します。このスタックを介してのみアクセスできるマトリックスが後で必要になる可能性があるため、このアプローチの使用はお勧めしません。また、GPUバスはCPUからGPUへの高速であるように設計されているため、使用は非常に長くなりますが、その逆はありません。
マスターオブジェクト
3Dシーンは、オブジェクトの依存関係を知るために、オブジェクトのツリーと見なされることがよくあります。このツリーのルート、オブジェクトのリスト、またはマスターオブジェクトに何を含めるべきかについては議論があります。
マスターオブジェクトを使用することをお勧めします。グラフィカルな表現はありませんが、再帰をより効果的に使用できるため、よりシンプルになります。
シーンマネージャーとレンダラーを分離します
私は@ejacに同意しません。OpenGL呼び出しを行う各オブジェクトにメソッドが必要です。シーンを参照する別のレンダラークラスを用意し、すべてのOpenGL呼び出しを実行すると、シーンロジックとOpenGLコードを分離するのに役立ちます。
これにより、設計が多少難しくなりますが、OpenGLからDirectXまたはその他のAPI関連に変更する必要がある場合は、柔軟性が高まります。
標準的な手法は、glPushAttrib/glPopAttribスコープ内のデフォルトのOpenGL状態からすべての変更を行うことにより、レンダリング状態に対するオブジェクトの影響を相互に分離することです。 C++では、コンストラクターを含むクラスを定義します。
glPushAttrib(GL_ALL_ATTRIB_BITS);
glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);
およびデストラクタを含む
glPopClientAttrib();
glPopAttrib();
そして、クラスRAIIスタイルを使用して、OpenGL状態を台無しにするコードをラップします。パターンに従っている場合、各オブジェクトのrenderメソッドは「クリーンな状態」になり、openGL状態の変更された可能性のあるすべてのビットを必要なものにすることを心配する必要はありません。
最適化として、通常、アプリの起動時にOpenGLの状態を、すべてが必要とするものに可能な限り近い状態に設定します。これにより、プッシュされたスコープ内で行う必要のある呼び出しの数が最小限に抑えられます。
悪いニュースは、これらは安い電話ではないということです。私はあなたが逃げることができる1秒あたりの数を実際に調査したことはありません。確かに複雑なシーンで役立つのに十分です。主なことは、状態を設定したら、状態を最大限に活用することです。鎧と肌に異なるシェーダー、テクスチャなどを使用してレンダリングするオークの軍隊がある場合は、鎧/肌/鎧/肌/ ...をレンダリングするすべてのオークを繰り返さないでください。鎧の状態を一度設定し、すべてのオークの鎧をレンダリングしてから、すべてのスキンをレンダリングするように設定してください。
自分でロールしたい場合、上記の回答で十分です。言及されている原則の多くは、ほとんどのオープンソースグラフィックエンジンに実装されています。 Scenegraphs は、ダイレクトモードのopengl描画から離れる1つの方法です。
OpenScenegraph は、OO 3Dグラフィックスを実行するためのツールの大規模な(多分大きすぎる)ライブラリを提供する1つのオープンソースアプリです。他にもたくさんあります。
私は通常、レンダリング可能なクラスごとに、opengl呼び出しを含むdrawOpenGl()関数を持っています。その関数はrenderloopから呼び出されます。このクラスは、opengl関数呼び出しに必要なすべての情報を保持しています。位置と向きについては、独自の変換を行うことができます。
オブジェクトが相互に依存している場合。それらはより大きなオブジェクトの一部を作り、そのオブジェクトを表す他のクラスでそれらのクラスを構成します。これには、子のすべてのdrawOpenGL()関数を呼び出す独自のdrawOpenGL()関数があるため、Pushmatrixとpopmatrixを使用して周囲の位置/方向の呼び出しを行うことができます。
しばらく経ちましたが、テクスチャでも同様のことが可能だと思います。
サーフェス法線と頂点法線を切り替えたい場合は、オブジェクトにどちらか一方を記憶させ、必要に応じてdrawOpenGL()が呼び出すたびに2つのプライベート関数を用意します。確かに他のよりエレガントな解決策があります(たとえば、戦略デザインパターンなどを使用する)が、これは私があなたの問題を理解している限りうまくいく可能性があります