W jaki sposób w systemie materiałów opartym na grafach mogę obsługiwać różne typy danych wejściowych i wyjściowych?


11

wprowadź opis zdjęcia tutaj

Próbuję owinąć głowę wokół jak systemy materialne, takie jak ta , to są realizowane. Te wydajne i przyjazne dla użytkownika, podobne do wykresów systemy wydają się być stosunkowo powszechne jako metoda umożliwiająca programistom i nieprogramiście szybkie tworzenie programów cieniujących. Jednak z mojego stosunkowo ograniczonego doświadczenia w programowaniu graficznym nie jestem do końca pewien, jak działają.


Tło:

Tak więc, kiedy wcześniej programowałem proste systemy renderowania OpenGL, zwykle tworzę klasę materiałów, która ładuje, kompiluje i łączy shadery ze statycznych plików GLSL, które ręcznie utworzyłem. Zwykle tworzę tę klasę jako proste opakowanie do dostępu do zmiennych mundurowych GLSL. Jako prosty przykład wyobraź sobie, że mam podstawowy moduł cieniujący wierzchołek i moduł cieniujący fragmenty, z dodatkowym jednolitym Texture2D do przekazywania tekstury. Moja klasa materiałów po prostu ładowałaby i kompilowała te dwa moduły cieniujące w materiał, i od tego momentu ujawniałaby prosty interfejs do odczytu / zapisu munduru Texture2D tego modułu cieniującego.

Aby uczynić ten system nieco bardziej elastycznym, zwykle piszę go w sposób, który pozwala mi próbować przekazać mundury o dowolnej nazwie / typie [np .: SetUniform_Vec4 („AmbientColor”, colorVec4); który ustawiłby mundur AmbientColor na konkretny wektor 4d o nazwie „colorVec4”, jeśli ten mundur istnieje w materiale.] .

class Material
{
    private:
       int shaderID;
       string vertShaderPath;
       string fragSahderPath;

       void loadShaderFiles(); //load shaders from files at internal paths.
       void buildMaterial(); //link, compile, buffer with OpenGL, etc.      

    public:
        void SetGenericUniform( string uniformName, int param );
        void SetGenericUniform( string uniformName, float param );
        void SetGenericUniform( string uniformName, vec4 param );
        //overrides for various types, etc...

        int GetUniform( string uniformName );
        float GetUniform( string uniformName );
        vec4 GetUniform( string uniformName );
        //etc...

        //ctor, dtor, etc., omitted for clarity..
}

To działa , ale wydaje się, jakby zły system ze względu na fakt, że klient Materiałowych klasa ma do mundurów dostępowych na samą wiarą - użytkownik musi być nieco świadomi mundurów, które są w każdym obiekcie materialnym, ponieważ są one zmuszone do przekazać je według nazwy GLSL. To nie jest wielka sprawa, gdy tylko 1-2 osoby pracują z systemem, ale nie mogę sobie wyobrazić, że ten system w ogóle skalowałby się bardzo dobrze, a zanim podejmę kolejną próbę programowania systemu renderującego OpenGL, chcę wyrównać trochę wyżej.


Pytanie:

Właśnie tam jestem, więc próbowałem zbadać, w jaki sposób inne silniki renderujące radzą sobie z ich systemami materiałowymi.

To podejście oparte na węzłach jest świetne i wydaje się być niezwykle powszechnym systemem do tworzenia przyjaznych dla użytkownika systemów materiałowych w nowoczesnych silnikach i narzędziach. Z tego, co mogę powiedzieć, są oparte na strukturze danych wykresu, gdzie każdy węzeł reprezentuje pewien aspekt cieniowania twojego materiału, a każda ścieżka reprezentuje pewien rodzaj relacji między nimi.

Z tego, co mogę powiedzieć, wdrożenie tego rodzaju systemu byłoby tak proste, jak klasa MaterialNode z różnymi podklasami (TextureNode, FloatNode, LerpNode itp.). Gdzie każda podklasa MaterialNode miałaby MaterialConnections.

class MaterialConnection
{
    MatNode_Out * fromNode;
    MatNode_In * toNode;
}

class LerpNode : MaterialNode
{
    MatNode_In x;
    MatNode_In y;
    MatNode_In alpha;

    MatNode_Out result;
}

To bardzo podstawowy pomysł, ale nie jestem pewien, jak działa kilka aspektów tego systemu:

1.) Jeśli spojrzysz na różne „wyrażenia materiałowe” (węzły), których używa Unreal Engine 4 , zobaczysz, że każdy z nich ma połączenia wejściowe i wyjściowe różnego rodzaju. Niektóre węzły wyjściowe są zmiennoprzecinkowe, niektóre wektor wyjściowy2, niektóre wektor wyjściowy4 itd. Jak mogę ulepszyć powyższe węzły i połączenia, aby mogły obsługiwać różne typy danych wejściowych i wyjściowych? Czy podklasowanie MatNode_Out z MatNode_Out_Float i MatNode_Out_Vec4 (i tak dalej) byłoby dobrym wyborem?

2.) Wreszcie, w jaki sposób ten system odnosi się do shaderów GLSL? Patrząc ponownie na UE4 (i podobnie w przypadku innych systemów połączonych powyżej), użytkownik musi ostatecznie podłączyć jakiś węzeł materiału do dużego węzła z różnymi parametrami reprezentującymi parametry modułu cieniującego (kolor podstawowy, metalowość, połysk, emisyjność itp.) . Moje pierwotne założenie było takie, że UE4 miał jakiś zakodowany „master shader” z różnymi mundurami, a wszystko, co użytkownik robi w swoim „materiale”, jest po prostu przekazywane do „master shadera”, kiedy podłączają swoje węzły do ​​„ węzeł główny ”.

Jednak dokumentacja UE4 stwierdza:

„Każdy węzeł zawiera fragment kodu HLSL, wyznaczony do wykonania określonego zadania. Oznacza to, że tworząc materiał, tworzysz kod HLSL za pomocą skryptów wizualnych”.

Jeśli to prawda, czy ten system generuje prawdziwy skrypt cieniujący? Jak dokładnie to działa?


1
Powiązane z twoim pytaniem: gameangst.com/?p=441
glampert

Odpowiedzi:


10

Spróbuję odpowiedzieć zgodnie z moją najlepszą wiedzą, z niewielką wiedzą na temat konkretnego przypadku UE4, ale raczej na temat ogólnej techniki.

Materiały oparte na grafach to tyle samo programowania, co samodzielne pisanie kodu. To po prostu nie jest tak dla ludzi bez doświadczenia w kodzie, co sprawia, że ​​wydaje się to łatwiejsze. Tak więc, gdy projektant łączy węzeł „Dodaj”, w zasadzie pisze add (wartość1, wartość2) i łączy dane wyjściowe z czymś innym. Oznacza to, że każdy węzeł wygeneruje kod HLSL, albo wywołanie funkcji, albo po prostu proste instrukcje.

W końcu użycie wykresu materiałowego jest jak programowanie surowych shaderów z biblioteką predefiniowanych funkcji, które wykonują niektóre typowe użyteczne rzeczy, i to samo robi UE4. Zawiera bibliotekę kodu modułu cieniującego, który kompilator modułu cieniującego pobiera i wstrzykuje do źródła ostatecznego modułu cieniującego, jeśli ma to zastosowanie.

W przypadku UE4, jeśli twierdzą, że jest przekonwertowany na HLSL, zakładam, że używają narzędzia konwertującego, które jest w stanie przekonwertować kod bajtowy HLSL na kod bajtowy GLSL, więc można go używać na platformach GL. Ale inne biblioteki mają po prostu wiele kompilatorów shaderów, które odczytują wykres i bezpośrednio generują potrzebne źródła języka cieniowania.

Graf materiałowy jest również dobrym sposobem na oderwanie się od specyfiki platformy i skupienie się na tym, co ważne z artystycznego punktu widzenia. Ponieważ nie jest związany z językiem i znacznie wyższy poziom, łatwiej jest go zoptymalizować pod kątem platformy docelowej i dynamicznie wstrzykiwać inny kod, np. Obsługę światła do modułu cieniującego.

1) Teraz, aby bardziej bezpośrednio odpowiedzieć na pytania, powinieneś zastosować podejście oparte na danych do projektowania takiego systemu. Znajdź płaski format, który można zdefiniować w bardzo prostych strukturach, a nawet zdefiniować w pliku tekstowym. Zasadniczo każdy wykres powinien być tablicą węzłów z typem, zestawem danych wejściowych i wyjściowych, a każde z tych pól powinno mieć lokalny identyfikator_dowiązania, aby upewnić się, że połączenia na wykresie są jednoznaczne. Ponadto każde z tych pól może mieć dodatkową konfigurację do obsługi tego pola (na przykład obsługiwany zakres typów danych).

Dzięki takiemu podejściu można łatwo zdefiniować pole węzła, które ma być (zmiennoprzecinkowe | podwójne) i pozwolić mu wywnioskować typ z połączeń lub zmusić do niego typ, bez hierarchii klas i nadinżynierii. To do Ciebie należy zaprojektowanie tej struktury danych wykresu tak sztywnej lub elastycznej, jak tylko chcesz. Wszystko czego potrzebujesz to to, że ma wystarczającą ilość informacji, aby generator kodu nie miał dwuznaczności, a zatem potencjalnie obsługiwał źle, co chcesz zrobić. Ważne jest, aby na poziomie podstawowej struktury danych zachować elastyczność i skupić się na rozwiązaniu zadania polegającego na samodzielnym zdefiniowaniu materiału.

Kiedy mówię „zdefiniuj materiał”, mam na myśli bardzo szczegółowo definiowanie powierzchni siatki, wykraczającej poza to, co zapewnia sama geometria. Obejmuje to stosowanie dodatkowych atrybutów wierzchołków do konfigurowania aspektu powierzchni, dodawania do niej przemieszczenia za pomocą mapy wysokości, zakłócania normalności za pomocą normalnych na piksel, zmiany parametrów fizycznych, zmiany BRDF i tak dalej. Nie chcesz opisywać niczego innego, jak HDR, mapowanie tonów, animacja skórowania, obsługa światła lub wiele innych rzeczy wykonywanych za pomocą shaderów.

2) Następnie do generatora modułu renderującego renderera należy przejście przez tę strukturę danych i odczytanie jej informacji, złożenie zestawu zmiennych i połączenie ich ze sobą za pomocą predefiniowanych funkcji i wstrzyknięcie kodu, który oblicza oświetlenie i inne efekty. Pamiętaj tylko, że moduły cieniujące różnią się nie tylko różnymi interfejsami graficznymi API, ale także między różnymi modułami renderującymi (odroczone renderowanie vs renderowanie oparte na kafelkach lub renderer do przodu wymagają różnych shaderów), a przy takim systemie materiałowym można wyodrębnić z nieprzyjemnych warstwa niskiego poziomu i skup się tylko na opisie powierzchni. ”

W przypadku UE4 wymyślili listę elementów dla wspomnianego końcowego węzła wyjściowego, który według nich opisuje 99% powierzchni w starych i nowoczesnych grach. Opracowali ten zestaw parametrów przez dziesięciolecia i udowodnili, że dzięki niesamowitej liczbie gier, które dotychczas wyprodukował silnik Unreal. Dlatego wszystko będzie dobrze, jeśli będziesz robił to samo, co nierzeczywiste.

Podsumowując, proponuję plik .material do obsługi każdego wykresu. Podczas programowania zawierałby być może tekstowy format do debugowania, a następnie zostałby spakowany lub skompilowany do wersji binarnej w celu wydania. Każdy .material składałby się z N węzłów i N połączeń, podobnie jak baza danych SQL. Każdy węzeł miałby N pól z nazwą i niektórymi flagami dla typów, które zostałyby zaakceptowane, jeśli jego dane wejściowe lub wyjściowe, jeśli typy są wywnioskowane, itp. Struktura danych wykonawczych do przechowywania załadowanego materiału byłaby równie płaska i prosta, więc edytor może łatwo go dostosować i zapisać z powrotem do pliku.

A potem pozostaw faktyczne podnoszenie ciężarów do ostatecznej generacji shaderów, co jest naprawdę najtrudniejszym zadaniem. Piękna część polega na tym, że Twój materiał pozostaje agnostyczny wobec platformy renderującej, teoretycznie działałby z dowolną techniką renderowania i interfejsem API, pod warunkiem, że reprezentujesz materiał w odpowiednim języku cieniowania.

Daj mi znać, jeśli potrzebujesz dodatkowych szczegółów lub jakichkolwiek poprawek w mojej odpowiedzi, straciłem przegląd całego tekstu.


Nie mogę ci wystarczająco podziękować za napisanie tak skomplikowanej i doskonałej odpowiedzi. Czuję, że mam świetny pomysł na to, dokąd powinienem się udać! Dzięki!
MrKatSwordfish

1
Nie ma problemu, kolego, napisz do mnie, jeśli potrzebujesz dalszej pomocy. Właściwie pracuję nad czymś równoważnym dla moich własnych narzędzi, więc jeśli chcesz wymieniać myśli, bądź moim gościem! Miłego popołudnia: D
Grimshaw,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.