Mostki agnostyczne API (tj. OpenGL / D3D / Whthing). Czy używasz ich, jak je robisz. Pro's i Con's [zamknięte]

12

Tworzysz silnik 3d. Chcesz najlepszych światów wieloplatformowych. Nagle zdajesz sobie sprawę, że jeśli chcesz używać Direct3D na komputerach z systemem Windows i OpenGL na OSX / Linux, musisz poświęcić obsługiwane funkcje obu najmniej powszechnym mianownikom.

Niektórzy mogą korzystać z OpenGL w trzech systemach operacyjnych, ponieważ sam w sobie wydaje się to najmniej wspólny mianownik. Wszystko jest dobrze. Następnie musisz przenieść backend API graficznego do GX Nintendo, musisz także utworzyć ścieżkę PS3 i Xbox360.

Co robisz? Czy projektujesz własny interfejs API, który sam w sobie jest najmniej wspólnym mianownikiem, i piszesz dla niego implementacje zaplecza dla każdej platformy, czy piszesz dla każdej platformy, która ma swój oddział?

Jeśli zdecydujesz się zaprojektować własny interfejs API, czy używasz wzorca mostu lub własnego voodoo? Gdzie kończy się szaleństwo, kiedy zdajesz sobie sprawę ze wszystkiego, a podejście do zlewu kuchennego musi się skończyć i w zasadzie masz oddzielny silnik dla każdej platformy jako oddziału. Lub trzymasz się wszystkiego i zlewu kuchennego i zachowujesz specyfikę platformy w specjalizacjach modułu zaplecza dla każdej platformy.

Klatka kluczowa
źródło

Odpowiedzi:

9

Nie jestem fanem najmniej powszechnego mianownika. Jeśli to zrobisz, możesz skończyć z kalekimi funkcjami i słabą wydajnością.

Zamiast tego, co zrobiłem w przeszłości, to zapewnienie nieco wyższej funkcjonalności w bibliotece. Ta biblioteka jest (głównie) niezależna od API i może być używana w dowolnym miejscu, ale implementacja biblioteki jest zupełnie inna dla różnych platform / backendów graficznych. Na przykład zamiast funkcji SetStateX () masz wyższe funkcje, takie jak RenderMesh () lub CreateRenderTarget ().

Za każdym razem, gdy przejdziesz na nową platformę, będzie to więcej pracy niż naprawdę cienkiej warstwy, ale będzie całkowicie tego warte, ponieważ będziesz w stanie wdrożyć rzeczy w optymalny sposób dla tej platformy i będziesz mógł wziąć zaletą rodzimych, unikalnych funkcji.

Jeszcze jedno: nie bój się lekkiego złamania kapsułkowania. Nie ma nic bardziej frustrującego niż wiedza, że ​​jesteś na platformie z pewnymi możliwościami i nie możesz z nich korzystać. Pozostawienie jakiegoś backdoora, aby kod wyższego poziomu mógł skorzystać z platformy, jest bardzo przydatne (na przykład możliwość odzyskania urządzenia D3D lub kontekstu OpenGL).

Noel Llopis
źródło
3
Myślę, że powiedziałeś to, co próbowałem powiedzieć, tylko lepiej.
AShelly,
6

Mogę tylko rzucić okiem na Ogre3D . Jest napisany w C ++, Open source (teraz licencja MIT) i działa na każdej większej platformie od razu po wyjęciu z pudełka. Wyodrębnia API renderowania i może przełączyć się z używania DirectX na OpenGL z kilkoma ustawieniami. Jednak nie wiem wystarczająco dużo o różnicach między zestawami funkcji DirectX i OpenGL, aby stwierdzić, że obsługuje lub nie obsługuje określonej funkcji.

Torchlight od Runic Games został napisany przy użyciu Ogre i grałem w to na komputerach Mac i PC i działa bardzo dobrze na obu.

Casey
źródło
2
+1 za podejście Ogra. Wiem o tym, przeczytałem część kodu. Bardziej zainteresowałem się osobistymi historiami na temat tego podejścia i tego, co robią inni ludzie w takiej sytuacji.
Klatka kluczowa
2
Dzięki! Zrobiłbym to głównie tak, jak Ogre. Stosowałem podejście interfejs / fabryka w wielu projektach międzyplatformowych i właściwie nie jestem pewien, jak bym to zrobił inaczej. Powiedziałbym, że absolutnie musisz pracować na wielu platformach jednocześnie. Nie próbuj kodować wszystkiego w systemie Windows, niż próbować na przykład przenieść na komputer Mac.
Casey
Tak! Zaczynało mi się naprawdę męczyć zwijaniem własnego owijarki z interfejsem API, a potem zacząłem używać Ogre. Nie oglądałem się za siebie. :)
jacmoe
1

Nie zrobiłem tego dla grafiki, ale stworzyłem wieloplatformowy zestaw narzędzi audio (PC / XBOX / PS2). Poszliśmy drogą tworzenia własnego interfejsu API z funkcją najmniej powszechnego mianownika, a także opcjonalnymi funkcjami specyficznymi dla platformy. Oto kilka wyciągniętych wniosków:

Kluczem jest zdefiniowanie ścieżki przetwarzania, która zawiera podstawowe możliwości każdej platformy i umożliwia rozwój. Aby to zrobić, musisz naprawdę zrozumieć niskopoziomowy interfejs API każdej platformy, aby móc zidentyfikować odpowiednie abstrakty. Upewnij się, że łańcuch działa na najmniej wydajnej platformie, zapewniając jednocześnie dostęp do zaawansowanych funkcji najskuteczniejszej formy. Wykonaj pracę, aby to naprawić, a zaoszczędzisz wiele wysiłku później.

W przypadku audio łańcuch był podobny SoundSources -> [Decoders] -> Buffers -> [3D Positioner] ->[Effects] -> Players.

Może to być grafika Model -> Shapes -> Positioner -> Texturer -> [Lighting] -> [Particle Effects] -> Renderer. (To chyba kompletnie zły zestaw, nie jestem facetem od grafiki).

Napisz interfejs API, który obsługuje Twoje podstawowe obiekty, oraz interfejs zaplecza specyficzny dla platformy, który odwzoruje interfejs API na funkcje niskiego poziomu. Zapewnij najlepszy wysiłek dla każdej możliwości. Na przykład na PC i XBOX pozycjonowanie dźwięku 3D przeprowadzono z wykorzystaniem możliwości HRTF chipów dźwiękowych, podczas gdy PS2 wykorzystywał proste przesuwanie i zanikanie. Silnik graficzny może zrobić coś podobnego z oświetleniem.

Zaimplementuj jak najwięcej interfejsu użytkownika, używając neutralnego kodu platformy. Kod do dołączania obiektu pogłosu do obiektu dźwiękowego lub zasobu tekstury do obiektu kształtu powinien być całkowicie powszechny, podobnie jak kod do iteracji i przetwarzania aktywnych obiektów. Z drugiej strony obiekty niskiego poziomu mogą być całkowicie specyficzne dla platformy, z wyjątkiem wspólnego interfejsu.

Upewnij się, że interfejs API lub pliki konfiguracyjne pozwalają użytkownikowi określić opcje specyficzne dla platformy. Staraliśmy się unikać pchania kodu specyficznego dla platformy na poziom gry, utrzymując go w plikach konfiguracyjnych: plik konfiguracyjny jednej platformy może określać „efekt: SuperDuperParticleGenerator”, podczas gdy inny mówi „efekt: SoftGlow”

Zdecydowanie rozwijaj platformy równolegle. Upewnij się, że interfejsy specyficzne dla platformy są dobrze zdefiniowane i można je testować samodzielnie. Zapobiega to często „czy to poziom platformy czy poziom interfejsu API?” problemy podczas debugowania.

AShelly
źródło
0

Piszę silnik gry open source o nazwie YoghurtGum dla platform mobilnych (Windows Mobile, Android). To był jeden z moich dużych problemów. Najpierw rozwiązałem to w ten sposób:

class RenderMethod
{

public:

  virtual bool Init();
  virtual bool Tick();
  virtual bool Render();

  virtual void* GetSomeData(); 

}

Czy zauważyłeś void*? Jest tak, ponieważ RenderMethodDirectDrawzwraca powierzchnię DirectDraw, a RenderMethodDirect3Dzwraca pulę wierzchołków. Wszystko inne również zostało podzielone. Miałem Spriteklasę, która miała albo SpriteDirectDrawwskaźnik, albo SpriteDirect3Dwskaźnik. To trochę do dupy.

Ostatnio ostatnio dużo przepisuję. To, co mam teraz, to RenderMethodDirectDraw.dlla RenderMethodDirect3D.dll. W rzeczywistości możesz spróbować użyć Direct3D, zawieść i zamiast tego użyć DirectDraw. To dlatego, że interfejs API pozostaje taki sam.

Jeśli chcesz stworzyć duszka, nie robisz tego bezpośrednio, ale poprzez fabrykę. Następnie fabryka wywołuje poprawną funkcję w bibliotece DLL i przekształca ją w element nadrzędny.

To jest w RenderMethodinterfejsie API:

virtual Sprite* CreateSprite(const char* a_Name) = 0;

A to jest definicja w RenderMethodDirectDraw:

Sprite* RenderMethodDirectDraw::CreateSprite(const char* a_Name)
{
    bool found = false;
    uint32 i;
    for (i = 0; i < m_SpriteDataFilled; i++)
    {
        if (!strcmp(m_SpriteData[i].name, a_Name))
        {
            found = true;
            break;
        }
    }

    if (!found) 
    {
        ERROR_EXPLAIN("Could not find sprite named '%s'", a_Name);
        return NULL; 
    }

    if (m_SpriteList[m_SpriteTotal]) { delete m_SpriteList[m_SpriteTotal]; }
    m_SpriteList[m_SpriteTotal] = new SpriteDirectDraw();

    ((SpriteDirectDraw*)m_SpriteList[m_SpriteTotal])->SetData(&m_SpriteData[i]);

    return (m_SpriteList[m_SpriteTotal++]);
}

Mam nadzieję, że to ma sens. :)

PS Chciałbym do tego użyć STL, ale nie ma wsparcia dla Androida. :(

Gruntownie:

  • Zachowaj każdy rendering we własnym kontekście. Albo DLL, albo biblioteka statyczna, albo tylko kilka nagłówków. Tak długo, jak masz RenderMethodX, SpriteX i StuffX, jesteś złoty.
  • Kradnij jak najwięcej ze źródła Ogre.

EDYCJA: Tak, sensowne jest posiadanie takich wirtualnych interfejsów. Jeśli pierwsza próba się nie powiedzie, możesz wypróbować inną metodę renderowania. W ten sposób możesz zachować wszystkie metody renderowania kodu w sposób agnostyczny.

knight666
źródło
1
Czy naprawdę ma sens posiadanie wirtualnego interfejsu, jeśli nigdy nie będziesz mieć aktywnej więcej niż jednej implementacji w tym samym czasie?
NocturnDragon
0

Lubię do tego używać SDL . Ma backendery renderera dla D3D, OpenGl, OpenGL ES i garść innych backendów specyficznych dla platformy, i jest dostępny dla różnego rodzaju platform i jest obecnie w fazie rozwoju, z powiązaniami z wieloma różnymi językami.

Wyodrębnia różne koncepcje renderera i udostępnia tworzenie wideo (a także obsługę dźwięku i wejścia oraz kilka innych rzeczy) w prostym, wieloplatformowym interfejsie API. Został zaprojektowany przez Sama Lantingę, jednego z głównych deweloperów Blizzarda, specjalnie w celu ułatwienia przenoszenia gier i tworzenia gier międzyplatformowych, dzięki czemu wiesz, że masz do czynienia z biblioteką wysokiej jakości.

Mason Wheeler
źródło