16

レンダリング コードに非常に大きな設計上の障害があります。基本的にこれは、API 固有のコード (OpenGL コードや DirectX など) を必要としないものです。今、私は問題を解決する方法について多くの方法を考えてきましたが、どれを使用すればよいのか、これらのアイデアをどのように改善すればよいのかわかりません.

簡単な例を挙げるために、例としてテクスチャを使用します。テクスチャは、GPU メモリ内のテクスチャを表すオブジェクトです。実装に関しては、特定の方法で似ている可能性があります。つまり、実装でGLuintまたはLPDIRECT3DTEXTURE9を使用してテクスチャに似ているかどうかです。

これを実際に実装するために私が考えた方法を次に示します。より良い方法があるかどうか、またはどの方法が別の方法よりも優れているかはまったくわかりません。


方法 1: 継承

継承を使用できますが、これがこの問題の最も明白な選択のようです。ただし、このメソッドには仮想関数が必要であり、Texture オブジェクトを作成するには TextureFactory クラスが必要です。newオブジェクトごとに を呼び出す必要がありTextureます (例: renderer->getTextureFactory()->create())。

この場合に継承を使用することをどのように考えているかを次に示します。

class Texture
{
public:

    virtual ~Texture() {}

    // Override-able Methods:
    virtual bool load(const Image&, const urect2& subRect);
    virtual bool reload(const Image&, const urect2& subRect);
    virtual Image getImage() const;

    // ... other texture-related methods, such as wrappers for
    // load/reload in order to load/reload the whole image

    unsigned int getWidth() const;
    unsigned int getHeight() const;
    unsigned int getDepth() const;

    bool is1D() const;
    bool is2D() const;
    bool is3D() const;

protected:

    void setWidth(unsigned int);
    void setHeight(unsigned int);
    void setDepth(unsigned int);

private:
    unsigned int _width, _height, _depth;
};

次に、OpenGL (またはその他の API 固有) テクスチャを作成するには、 などのサブクラスを作成する必要がありますOglTexture

方法 2: 「TextureLoader」またはその他のクラスを使用する

この方法は見た目ほど簡単ですが、別のクラスを使用してテクスチャの読み込みを処理します。これは、状況に応じて(または必要と感じるかどうかに応じて)、仮想関数を使用する場合と使用しない場合があります。

例: ポリモーフィック テクスチャ ローダー

 class TextureLoader
 {
 public:

      virtual ~TextureLoader() {}


      virtual bool load(Texture* texture, const Image&, const urect2& subRect);
      virtual bool reload(Texture* texture, const Image&, const urect2& subRect);
      virtual Image getImage(Texture* texture) const;
 };

これを使用すると、Textureオブジェクトは POD タイプのみになります。ただし、これを機能させるには、ハンドル オブジェクト/ID がTextureクラス内に存在する必要があります。

たとえば、これは私が実装する可能性が高い方法です。ただし、基本クラスを使用して、ID 全体を一般化できる場合があります。Resourceグラフィックス リソースの ID を保持する基本クラスなど。

方法 3: Pimpl イディオム

ロード/リロードなどの方法を実装する pimpl イディオムを使用できます。テクスチャ。これには、テクスチャを作成するための抽象ファクトリ クラスが必要になる可能性が高くなります。これが継承を使用するよりも優れているかどうかはわかりません。この pimpl イディオムは方法 2 と組み合わせて使用​​できます。つまり、Texture オブジェクトはローダーへの参照 (ポインター) を持ちます。

方法 4: 概念/コンパイル時のポリモーフィズムを使用する

一方、コンパイル時のポリモーフィズムを使用して、仮想関数を宣言しないことを除いて、基本的に継承メソッドで提示したものを使用できます。これは機能しますが、OpenGL レンダリングから DirectX レンダリングに動的に切り替えたい場合、これは最適なソリューションではありません。OpenGL/D3D 固有のコードを Texture クラス内に配置するだけで、同じインターフェイス (load/reload/getImage/etc.) を持つ複数のテクスチャ クラスが存在し、名前空間内にラップされます (使用する API に似ています。ogl、などd3d)。

方法 5: 整数を使用する

テクスチャ オブジェクトへのハンドルを格納するために整数を使用することもできます。これはかなり単純に思えますが、「厄介な」コードが生成される可能性があります。


この問題は、Geometry、Shaders、ShaderPrograms などの他の GPU リソースにも存在します。

また、グラフィカル リソースの作成、読み込みなどを Renderer クラスで処理することも考えました。ただし、これはSPRに違反します。例えば

Texture* texture = renderer->createTexture(Image("something.png"));
Image image = renderer->getImage(texture);

誰かが私を案内してくれませんか、私はこれについて考えすぎていると思います。Irrlicht、Ogre3D など、オンラインで見つけたさまざまなレンダリング エンジンを観察してみました。Ogre と Irrlicht は継承を使用していますが、これが最善の方法かどうかはわかりません。void* や整数を使用したり、API 固有の (主に OpenGL) コードをクラス内に配置したりするものもあります (たとえば、GLuint を Texture クラス内に直接配置するなど)。どのデザインが自分に最も適しているかを本当に決めることはできません.

対象とするプラットフォームは次のとおりです。

  • Windows/Linux/Mac
  • iOS
  • おそらくアンドロイド

OpenGL はこれらすべてのプラットフォームで機能するため、OpenGL 固有のコードのみを使用することを検討しました。ただし、そうすると、PS3 などの OpenGL を使用できない他のプラットフォームに移植したい場合、コードをかなり変更する必要があると感じています。私の状況に関するアドバイスをいただければ幸いです。

4

3 に答える 3

12

高い視点から考えてみてください。レンダリング コードは、ゲーム/アプリケーション モデルの残りの部分とどのように連携しますか? 言い換えれば、シーン内でオブジェクトをどのように作成し、どの程度モジュール化する予定ですか? 私の以前のエンジンに関する研究では、適切に設計されたエンジンの最終結果は、通常、パターンに沿った段階的な手順を踏んでいます。例えば:

//Components in an engine could be game objects such as sprites, meshes, lights, audio sources etc. 
//These resources can be created via component factories for convenience
CRenderComponentFactory* pFactory = GET_COMPONENT_FACTORY(CRenderComponentFactory);

コンポーネントが取得されると、通常、オブジェクトを構築するために使用できるさまざまなオーバーロードされたメソッドがあります。例としてスプライトを使用するとSpriteComponent、サブコンポーネントの形式でスプライトが潜在的に必要とするすべてのものを a に含めることができます。TextureComponentたとえばのように。

//Create a blank sprite of size 100x100 
SpriteComponentPtr pSprite = pFactory->CreateSpriteComponent(Core::CVector2(100, 100));

//Create a sprite from a sprite sheet texture page using the given frame number.
SpriteComponentPtr pSprite = pFactory->CreateSpriteComponent("SpriteSheet", TPAGE_INDEX_SPRITE_SHEET_FRAME_1);

//Create a textured sprite of size 100x50, where `pTexture` is your TextureComponent that you've set-up elsewhere.
SpriteComponentPtr pSprite = pFactory->CreateSpriteComponent(Core::CVector2(100, 50), pTexture);

あとは、オブジェクトをシーンに追加するだけです。これは、エンティティを作成することで実現できます。これは、シーンの操作に必要なすべてを含む情報の単なる一般的なコレクションです。位置、向きなど。シーン内のすべてのエンティティについて、AddEntityメソッドはデフォルトでその新しいエンティティをレンダリング ファクトリに追加し、サブコンポーネントから他のレンダリング依存情報を抽出します。例えば:

//Put our sprite onto the scene to be drawn
pSprite->SetColour(CColour::YELLOW);
EntityPtr pEntity = CreateEntity(pSprite);
mpScene->AddEntity(pEntity);

これにより、オブジェクトを作成する優れた方法と、'draw' やその他のレンダリング固有のコードを参照することなくアプリケーションをコーディングするモジュラーな方法が得られます。優れたグラフィックス パイプラインは、次のようなものである必要があります。

ここに画像の説明を入力

これは、レンダリング エンジンの設計に役立つ優れたリソースです (上の画像の元でもあります)。21 ページにジャンプして、シーングラフの動作方法と一般的なエンジン設計理論の詳細な説明を参照してください。

于 2013-03-23T11:21:52.523 に答える
6

ここに正解はないと思いますが、私だったら次のようにします。

  1. 最初は OpenGL のみを使用することを計画してください。

  2. レンダリング コードを他のコードから分離しておきます (これは良い設計です)。ただし、追加の抽象化レイヤーでラップしようとしないでください。OpenGL にとって最も自然なことは何でもしてください。

  3. PS3 に移植する場合、レンダリング コードに何をする必要があるかをよりよく把握できるのでリファクタリングしてより抽象的なインターフェイスを引き出すのに適した時期になると考えてください。

于 2013-03-21T13:29:53.940 に答える
3

私は、方法 (2)、(3)、(5)、および将来的には (4) のハイブリッド アプローチを採用することにしました。

私が基本的に行ったことは次のとおりです。

すべてのリソースにはハンドルが関連付けられています。このハンドルは、オブジェクトを記述します。各ハンドルには ID が関連付けられており、これは単純な整数です。各リソースで GPU と対話するために、各ハンドルのインターフェイスが作成されます。このインターフェイスは現時点では抽象的ですが、将来そうすることにした場合は、テンプレートを使用して行うことができます。リソース クラスには、インターフェイスへのポインターがあります。

簡単に言うと、ハンドルは実際の G​​PU オブジェクトを表し、リソースはハンドルの単なるラッパーであり、ハンドルと GPU を接続するインターフェイスです。

これは基本的に次のようになります。

// base class for resource handles
struct ResourceHandle
{  
   typedef unsigned Id;
   static const Id NULL_ID = 0;
   ResourceHandle() : id(0) {}

   bool isNull() const
   { return id != NULL_ID; }

   Id id;
};

// base class of a resource
template <typename THandle, typename THandleInterface>
struct Resource
{
    typedef THandle Handle;
    typedef THandleInterface HandleInterface;

    HandleInterface* getInterface() const { return _interface; }
    void setInterface(HandleInterface* interface) 
    { 
        assert(getHandle().isNull()); // should not work if handle is NOT null
        _interface = interface;
    }

    const Handle& getHandle() const
    { return _handle; }

protected:

    typedef Resource<THandle, THandleInterface> Base;

    Resource(HandleInterface* interface) : _interface(interface) {}

    // refer to this in base classes
    Handle _handle;

private:

    HandleInterface* _interface;
};

これにより、非常に簡単に拡張でき、次のような構文が可能になります。

Renderer renderer;

// create a texture
Texture texture(renderer);

// load the texture
texture.load(Image("test.png");

Textureは から派生し、レンダラーResource<TextureHandle, TextureHandleInterface>はテクスチャ ハンドル オブジェクトをロードするための適切なインターフェイスを備えています。

私はこれの短い実例をここに持っています。

これが機能することを願っています。将来的に再設計することを選択する可能性があります。その場合は更新します。批判を歓迎します。

編集:

私は実際にこれを行う方法を再び変更しました。私が使用しているソリューションは、上記のものと非常に似ていますが、次のように異なります。

  1. API は「バックエンド」を中心に展開します。これらは、共通のインターフェイスを持ち、低レベル API (Direct3D や OpenGL など) と通信するオブジェクトです。
  2. ハンドルは整数/ID ではなくなりました。texture_handle_typeバックエンドには、リソース ハンドル タイプ ( 、program_handle_type、 など)ごとに特定の typedef がありますshader_handle_type
  3. リソースには基本クラスがなく、1 つのテンプレート パラメーター (a GraphicsBackend) のみが必要です。リソースは、それが属するグラフィック バックエンドへのハンドルと参照を格納します。次に、リソースには使いやすい API があり、ハンドルとグラフィック バックエンドの共通インターフェイスを使用して「実際の」リソースと対話します。つまり、リソース オブジェクトは基本的に、RAII を可能にするハンドルのラッパーです。
  4. リソースの構築を可能にするため、graphics_device オブジェクトが導入されました (工場device.createTexture()パターンdevice.create<my_device_type::texture>()

例えば:

#include <iostream>
#include <string>
#include <utility>

struct Image { std::string id; };

struct ogl_backend
{
    typedef unsigned texture_handle_type;

    void load(texture_handle_type& texture, const Image& image)
    {
        std::cout << "loading, " << image.id << '\n';
    }

    void destroy(texture_handle_type& texture)
    {
        std::cout << "destroying texture\n";
    }
};

template <class GraphicsBackend>
struct texture_gpu_resource
{
    typedef GraphicsBackend graphics_backend;
    typedef typename GraphicsBackend::texture_handle_type texture_handle;

    texture_gpu_resource(graphics_backend& backend)
        : _backend(backend)
    {
    }

    ~texture_gpu_resource()
    {
        // should check if it is a valid handle first
        _backend.destroy(_handle);
    }

    void load(const Image& image)
    {
        _backend.load(_handle, image);
    }

    const texture_handle& handle() const
    {
        return _handle;
    }

private:

    graphics_backend& _backend;
    texture_handle _handle;
};


template <typename GraphicBackend>
class graphics_device
{
    typedef graphics_device<GraphicBackend> this_type;

public:

    typedef texture_gpu_resource<GraphicBackend> texture;

    template <typename... Args>
    texture createTexture(Args&&... args)
    {
        return texture{_backend, std::forward(args)...};
    }

    template <typename Resource, typename... Args>
    Resource create(Args&&... args)
    {
             return Resource{_backend, std::forward(args)...};
        }

private:

    GraphicBackend _backend;
};


class ogl_graphics_device : public graphics_device<ogl_backend>
{
public:

    enum class feature
    {
        texturing
    };

    void enableFeature(feature f)
    {
        std::cout << "enabling feature... " << (int)f << '\n';
    }
};


// or...
// typedef graphics_device<ogl_backend> ogl_graphics_device


int main()
{
    ogl_graphics_device device;

    device.enableFeature(ogl_graphics_device::feature::texturing);

    auto texture = device.create<decltype(device)::texture>();

    texture.load({"hello"});

    return 0;
}

/*

 Expected output:
    enabling feature... 0
    loading, hello
    destroying texture

*/

ライブデモ: http://ideone.com/Y2HqlY

このデザインは現在、私のライブラリrojoで使用されています(注:このライブラリはまだ大規模な開発中です)。

于 2013-04-16T10:51:37.953 に答える