Rendering Engine Design-abstrakcja kodu API dla zasobów

Mam bardzo dużą przeszkodę w projekcie w moim kodzie renderującym. Zasadniczo to, co to jest, nie wymaga specyficznego kodu API (takiego jak kod OpenGL lub DirectX). Teraz myślałem o wielu sposobach, jak rozwiązać problem, jednak nie jestem pewien który z nich użyć, lub jak powinienem poprawić te pomysły.

Aby podać krótki przykład, użyję tekstury jako przykładu. Tekstura jest obiektem, który reprezentuje teksturę w pamięci GPU, jeśli chodzi o implementację, może to być przypominał w jakiś szczególny sposób, tzn. czy implementacja używa GLuint Czy LPDIRECT3DTEXTURE9 do przypominania tekstury.

Oto sposoby, które wymyśliłem, aby to właściwie wdrożyć. Nie jestem pewien, czy jest lepszy sposób, Czy który jest lepszy od innego.


Metoda 1: Dziedziczenie

Przydałoby mi się dziedziczenie, wydaje się to najbardziej oczywistym wyborem w tej sprawie. Jednak ta metoda wymaga wirtualnych funkcji i wymagałaby klasy TextureFactory w celu tworzenia obiektów tekstur. Które wymagałyby wywołań do new dla każdego Texture obiektu (np. renderer->getTextureFactory()->create()).

Oto jak myślę o użyciu dziedziczenia w tym przypadku:

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

A następnie, aby móc tworzyć Tekstury OpenGL (lub inne specyficzne dla API), musiałaby zostać utworzona podklasa, taka jak OglTexture.

Metoda 2: Użyj 'TextureLoader' lub innej klasy

Ta metoda jest tak prosta, jak się wydaje, używam innej klasy do obsługi ładowania tekstur. To może lub nie może używać funkcji wirtualnych, w zależności od okoliczności (lub czy uważam, że jest to konieczne).

Np. ładowarka tekstur polimorficznych

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

Gdybym miał tego użyć, Texture obiekt byłby tylko typem POD. jednakże , aby to zadziałało, obiekt/ID obsługi musiałby być obecny w Texture klasie.

Na przykład, w ten sposób najprawdopodobniej zaimplementowałbym to . Chociaż, być może będę w stanie uogólnić cały identyfikator rzecz, używając klasy bazowej. Na przykład klasa bazowa Resource, w której to przypadku Przechowuje identyfikator zasobu graficznego.

Metoda 3: Idiom Pimpl

Mógłbym użyć idiomu pimpl, który implementuje jak załadować / przeładować / etc. tekstury. Prawdopodobnie wymagałoby to klasy abstrakcyjnej fabryki do tworzenia tekstur. Nie jestem pewien, jak to jest lepsze niż korzystanie z dziedziczenia. Ten idiom pimpl może być używany w połączeniu z metodą 2, tzn. Obiekty Tekstury będą miały odniesienie (wskaźnik) do ich ładowacz.

Metoda 4: wykorzystanie pojęć / polimorfizmu w czasie kompilacji

Mógłbym z drugiej strony użyć polimorfizmu w czasie kompilacji i zasadniczo użyć tego, co przedstawiłem w metodzie dziedziczenia, z wyjątkiem bez deklarowania funkcji wirtualnych. To by zadziałało, ale gdybym chciał dynamicznie przełączyć się z renderowania OpenGL na renderowanie DirectX, nie byłoby to najlepsze rozwiązanie. Po prostu umieściłbym kod specyficzny dla OpenGL / D3D w klasie Texture, gdzie byłoby wiele tekstur klasy z niektórymi-co ten sam interfejs (load/reload/getImage / etc.), zawinięte wewnątrz jakiejś przestrzeni nazw (przypominające, którego API używa, np. ogl, d3d, itd.).

Metoda 5: używanie liczb całkowitych

Mógłbym po prostu użyć liczb całkowitych do przechowywania uchwytów do obiektów tekstury, wydaje się to dość proste, ale może spowodować jakiś "bałagan" kod.


Ten problem występuje również w przypadku innych zasobów GPU, takich jak geometria, shadery i Shaderprogramy.

Ja też myślałem o wykonanie klasy Renderer zajmującej się tworzeniem, ładowaniem itp. zasobów graficznych. Jednakże naruszałoby to SPR. np.

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

Czy ktoś może mnie poprowadzić, myślę, że zbyt mocno o tym myślę. Próbowałem obserwować różne silniki renderujące, takie jak Irrlicht, Ogre3D i inne, które znalazłem w Internecie. Ogre i Irrlicht używają dziedziczenia, jednak nie jestem pewien, czy jest to najlepsza droga do podjęcia. Jak niektórzy inni po prostu używają void*, liczb całkowitych lub po prostu umieszczają Kod specyficzny dla API (głównie OpenGL) w obrębie ich klas (np. GLuint bezpośrednio w klasie Texture). Naprawdę nie mogę się zdecydować, który projekt będzie dla mnie najbardziej odpowiedni.

Platformy, na które mam zamiar celować to:

  • Windows / Linux / Mac
  • iOS
  • Prawdopodobnie Android

Rozważałem użycie kodu specyficznego dla OpenGL, ponieważ OpenGL działa na wszystkich tych platformach. Jednak czuję, że jeśli to zrobię, będę musiał zmienić mój kod całkiem dużo jeśli chcę portować na inne platformy, które nie mogą korzystać z OpenGL, takie jak PS3. Każda rada na temat mojej sytuacji będzie bardzo mile widziana.

Author: miguel.martin, 2013-03-21

3 answers

Pomyśl o tym z wysokiego poziomu. Jak będzie działał Twój kod renderujący z resztą modelu Gry/aplikacji? Innymi słowy, jak planujesz tworzyć obiekty w swojej scenie i w jakim stopniu modułowość? W mojej poprzedniej pracy z silnikami, efekt końcowy dobrze zaprojektowanego silnika na ogół ma procedurę krok po kroku, która podąża za wzorem. Na przykład:

//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);

Po uzyskaniu komponentu są zazwyczaj różne przeciążone metody, których możesz użyć aby skonstruować obiekt. Używając sprite 'a jako przykładu, SpriteComponent może zawierać wszystko, co potencjalnie potrzebne do sprite' a w postaci pod-komponentów; na przykład 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);

To po prostu kwestia dodania obiektu do sceny. Można to zrobić poprzez stworzenie podmiotu, który jest po prostu ogólnym zbiorem informacji, który zawiera wszystko, co potrzebne do manipulacji sceną; pozycję, orientację itp. Dla każdej istoty w Twojej scenie, twoja metoda AddEntity dodałaby ta nowa jednostka domyślnie jest fabrycznie renderowana, wydobywając inne zależne od renderowania informacje z podskładników. Np.:

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

To, co masz, to ładny sposób tworzenia obiektów i modułowy sposób kodowania aplikacji bez konieczności odwoływania się do "rysowania" lub innego kodu specyficznego dla renderowania. Dobry rurociąg graficzny powinien być czymś w stylu:

Tutaj wpisz opis obrazka

This is a nice resource for rendering engine design (also where the above image is od). Przejdź do strony 21 i czytaj dalej, gdzie zobaczysz dogłębne wyjaśnienia dotyczące działania scenegraphs i ogólnej teorii projektowania silnika.

 11
Author: KillAWatt1705,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-23 13:00:52

Myślę, że nie ma tu żadnej poprawnej odpowiedzi, ale gdybym to był ja, zrobiłbym to:

  1. Planuj używać tylko OpenGL na początek.

  2. Renderuj kod oddzielnie od innych (to po prostu dobry projekt), ale nie próbuj owijać go w dodatkową warstwę abstrakcji - po prostu rób to, co jest najbardziej naturalne dla OpenGL.

  3. Pomyślałem, że jeśli i kiedy będę przenosił na PS3, będę miał o wiele lepsze zrozumienie tego, do czego potrzebuje mój kod renderujący, więc to byłby właściwy czas na refaktoryzację i wyciągnięcie bardziej abstrakcyjnego interfejsu.

 6
Author: Russell Zahniser,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-21 13:29:53

Zdecydowałem się na podejście hybrydowe, z metodą (2), (3), (5) i ewentualnie (4) w przyszłości.

To co w zasadzie zrobiłem to:

Każdy zasób ma dołączony uchwyt. Ten uchwyt opisuje obiekt. Każdy uchwyt ma powiązany z nim identyfikator, który jest prostą liczbą całkowitą. Aby rozmawiać z GPU Z Każdym zasobem, tworzony jest interfejs dla każdego uchwytu. Ten interfejs jest w tej chwili abstrakcyjny, ale można to zrobić za pomocą szablonów, jeśli zdecyduję się to zrobić w przyszłość. Klasa zasobów ma wskaźnik do interfejsu.

Mówiąc najprościej, uchwyt opisuje rzeczywisty obiekt GPU, a zasób to tylko owijka nad uchwytem i interfejs do połączenia uchwytu i GPU razem.

Tak to w zasadzie wygląda:

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

Pozwala mi to dość łatwo rozszerzyć i pozwala na składnię taką jak:

Renderer renderer;

// create a texture
Texture texture(renderer);

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

Gdzie Texture pochodzi z Resource<TextureHandle, TextureHandleInterface> i gdzie renderer ma odpowiedni interfejs do ładowania tekstury obsługują obiekty.

Mam krótki działający przykład tego tutaj .

Mam nadzieję, że to zadziała, mogę zdecydować się na przeprojektowanie go w przyszłości, jeśli tak, zaktualizuję. Krytyka byłaby mile widziana.

EDIT:

Zmieniłem sposób, w jaki to robię. Rozwiązanie, którego używam, jest dość podobne do opisanego powyżej, ale tutaj jest inaczej:

  1. API obraca się wokół "backendów" , są to obiekty, które posiadają wspólny interfejs i komunikują się z niskopoziomowym API (np. Direct3D lub OpenGL).
  2. Uchwyty nie są już liczbami całkowitymi/identyfikatorami. Backend ma specyficzne typedef dla każdego typu obsługi zasobów (np. texture_handle_type, program_handle_type, shader_handle_type).
  3. zasoby nie mają klasy bazowej i wymagają tylko jednego parametru szablonu (a GraphicsBackend). Zasób przechowuje uchwyt i odniesienie do zaplecza graficznego, do którego należy. Następnie zasób ma przyjazne dla użytkownika API i korzysta z wspólnego zaplecza obsługi i Grafiki interfejs do interakcji z "rzeczywistym" zasobem. tzn. resource objects są w zasadzie opakowaniami uchwytów, które pozwalają na RAII.
  4. obiekt graphics_device jest wprowadzony w celu umożliwienia budowy zasobów (wzorzec fabryczny; np. device.createTexture() lub device.create<my_device_type::texture>(),

Na przykład:

#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

*/

Live demo: http://ideone.com/Y2HqlY

Ten projekt jest obecnie używany w mojej bibliotece rojo (Uwaga: ta Biblioteka jest nadal pod ciężkim rozwój).

 3
Author: miguel.martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-12 02:24:52