Próbuję stworzyć silnik do gier 2D za pomocą OpenGL ES 2.0 (na razie iOS). Napisałem warstwę aplikacji w celu C i osobny samodzielny RendererGLES20 w C ++. Poza rendererem nie jest wykonywane żadne specyficzne wywołanie GL. Działa idealnie.
Ale mam pewne problemy z projektowaniem podczas korzystania z shaderów. Każdy moduł cieniujący ma swoje unikalne atrybuty i mundury, które należy ustawić tuż przed głównym wywołaniem losowania (w tym przypadku glDrawArrays). Na przykład, aby narysować geometrię, zrobiłbym:
void RendererGLES20::render(Model * model)
{
// Set a bunch of uniforms
glUniformMatrix4fv(.......);
// Enable specific attributes, can be many
glEnableVertexAttribArray(......);
// Set a bunch of vertex attribute pointers:
glVertexAttribPointer(positionSlot, 2, GL_FLOAT, GL_FALSE, stride, m->pCoords);
// Now actually Draw the geometry
glDrawArrays(GL_TRIANGLES, 0, m->vertexCount);
// After drawing, disable any vertex attributes:
glDisableVertexAttribArray(.......);
}
Jak widać, ten kod jest wyjątkowo sztywny. Gdybym miał użyć innego modułu cieniującego, powiedzmy efekt falowania, musiałbym przekazać dodatkowe mundury, atrybuty wierzchołków itp. Innymi słowy, musiałbym zmienić kod źródłowy renderowania RendererGLES20, aby włączyć nowy moduł cieniujący.
Czy istnieje sposób, aby obiekt modułu cieniującego był całkowicie ogólny? Co jeśli chcę po prostu zmienić obiekt modułu cieniującego i nie martwić się ponowną kompilacją źródła gry? Jakikolwiek sposób na uczynienie renderera agnostycznym dla mundurów i atrybutów itp.? Nawet jeśli musimy przekazywać dane do mundurów, jakie jest najlepsze miejsce do tego? Klasa modelowa? Czy klasa modelu zna mundury i atrybuty modułu cieniującego?
Następujące pokazy Klasa aktora:
class Actor : public ISceneNode
{
ModelController * model;
AIController * AI;
};
Model kontrolera klasy: klasa ModelController {klasa IShader * moduł cieniujący; int textureId; odcień vec4; pływak alfa; struct Vertex * vertexArray; };
Klasa Shader zawiera tylko obiekt modułu cieniującego, kompilację i łączenie podprogramów itp.
W klasie Game Logic faktycznie renderuję obiekt:
void GameLogic::update(float dt)
{
IRenderer * renderer = g_application->GetRenderer();
Actor * a = GetActor(id);
renderer->render(a->model);
}
Pamiętaj, że chociaż Actor rozszerza ISceneNode, nie rozpocząłem jeszcze wdrażania SceneGraph. Zrobię to, jak tylko rozwiążę ten problem.
Wszelkie pomysły, jak to poprawić? Powiązane wzorce projektowe itp.?
Dziękuję za przeczytanie pytania.
źródło
Odpowiedzi:
Możliwe jest, aby twój system cieniujący był bardziej oparty na danych, dzięki czemu nie masz tak dużo kodu specyficznego dla modułu cieniującego dla mundurów i formatów wierzchołków, ale raczej ustawiasz je programowo na podstawie metadanych dołączonych do modułów cieniujących.
Najpierw zastrzeżenie: system oparty na danych może ułatwić dodawanie nowych shaderów, ale z drugiej strony wiąże się z kosztami związanymi ze zwiększoną złożonością systemu, co utrudnia utrzymanie i debugowanie. Dlatego dobrym pomysłem jest dokładne zastanowienie się, jak duże będzie kierowanie danymi (dla małego projektu odpowiedź może być „brak”) i nie próbuj budować systemu, który jest zbyt ogólny.
Dobra, porozmawiajmy najpierw o formatach wierzchołków (atrybutach). Możesz utworzyć opis danych, tworząc strukturę zawierającą dane do przekazania
glVertexAttribPointer
- indeks, typ, rozmiar itp. Pojedynczego atrybutu - i posiadając tablicę tych struktur do reprezentowania całego formatu wierzchołków. Biorąc pod uwagę te informacje, możesz programowo ustawić cały stan GL związany z atrybutami wierzchołka.Skąd pochodzą dane do wypełnienia tego opisu? Pod względem koncepcyjnym uważam, że najczystszym sposobem jest przypisanie go do modułu cieniującego. Podczas budowania danych wierzchołków dla siatki należy sprawdzić, który moduł cieniujący jest używany na siatce, znaleźć format wierzchołków wymagany przez ten moduł cieniujący i odpowiednio zbudować bufor wierzchołków. Potrzebujesz tylko pewnego sposobu, aby każdy moduł cieniujący określił oczekiwany format wierzchołków.
Można to zrobić na wiele sposobów; na przykład, możesz mieć standardowy zestaw nazw dla atrybutów w module cieniującym („attrPosition”, „attrNormal” itp.) oraz niektóre sztywne reguły, takie jak „pozycja to 3 zmiennoprzecinkowe”. Następnie używasz
glGetAttribLocation
lub podobnie, aby zapytać, które atrybuty używa moduł cieniujący, i zastosować reguły do budowy formatu wierzchołków. Innym sposobem jest posiadanie fragmentu kodu XML określającego format, osadzonego w komentarzu w źródle modułu cieniującego i wyodrębnionego przez narzędzia lub coś w tym stylu.W przypadku mundurów, jeśli możesz użyć OpenGL 3.1 lub nowszej wersji, dobrym pomysłem jest użycie jednolitych obiektów buforowych (odpowiednik OpenGL stałych buforów D3D). Niestety, GL ES 2.0 ich nie ma, więc mundury należy traktować indywidualnie. Jednym ze sposobów, aby to zrobić, byłoby utworzenie struktury zawierającej jednolitą lokalizację dla każdego parametru, który chcesz ustawić - matrycę kamery, moc światła, matrycę świata itp. Tutaj mogą być również lokalizacje próbników. To podejście zależy od istnienia standardowego zestawu parametrów wspólnych dla wszystkich shaderów. Nie każdy moduł cieniujący musi korzystać z każdego parametru, ale wszystkie parametry muszą znajdować się w tej strukturze.
Każdy moduł cieniujący miałby instancję tej struktury, a gdy ładujesz moduł cieniujący, pytasz go o lokalizację wszystkich parametrów, używając
glGetUniformLocation
standardowych nazw. Następnie, ilekroć musisz ustawić jednolity kod, możesz sprawdzić, czy jest on obecny w tym module cieniującym, po prostu sprawdź jego lokalizację i ustaw go.źródło