Ich versuche , meinen Kopf herum zu wickeln , wie Materialsysteme wie diese , diese umgesetzt werden. Diese leistungsstarken und benutzerfreundlichen grafischen Systeme scheinen relativ häufig zu sein, um Programmierern und Nicht-Programmierern das schnelle Erstellen von Shadern zu ermöglichen. Aufgrund meiner relativ begrenzten Erfahrung mit Grafikprogrammierung bin ich mir jedoch nicht ganz sicher, wie sie funktionieren.
Hintergrund:
Wenn ich zuvor einfache OpenGL-Rendering-Systeme programmiert habe , erstelle ich normalerweise eine Materialklasse, die Shader aus statischen GLSL-Dateien lädt, kompiliert und verknüpft, die ich manuell erstellt habe. Normalerweise erstelle ich diese Klasse auch als einfachen Wrapper für den Zugriff auf einheitliche GLSL-Variablen. Stellen Sie sich als einfaches Beispiel vor, ich habe einen einfachen Vertex-Shader und Fragment-Shader mit einer extra einheitlichen Texture2D zum Übergeben einer Textur. Meine Material-Klasse würde diese beiden Shader einfach laden und zu einem Material kompilieren und von diesem Punkt an eine einfache Schnittstelle zum Lesen / Schreiben der Texture2D-Uniform dieses Shaders bereitstellen.
Um dieses System ein wenig flexibler zu gestalten, schreibe ich es normalerweise so, dass ich versuchen kann, Uniformen eines beliebigen Namens / Typs zu übergeben [dh: SetUniform_Vec4 ("AmbientColor", colorVec4); Dies würde die AmbientColor-Uniform auf einen bestimmten 4d-Vektor namens "colorVec4" setzen, wenn diese Uniform im Material vorhanden ist.] .
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..
}
Dies funktioniert, aber es fühlt sich wie ein schlechtes System an, da der Kunde der Materialklasse nur im Glauben auf Uniformen zugreifen muss - der Benutzer muss sich der Uniformen in jedem Materialobjekt einigermaßen bewusst sein, weil er dazu gezwungen ist Übergeben Sie sie mit ihrem GLSL-Namen. Es ist keine große Sache, wenn nur 1-2 Personen mit dem System arbeiten, aber ich kann mir nicht vorstellen, dass dieses System überhaupt sehr gut skaliert werden kann, und bevor ich meinen nächsten Versuch unternehme, ein OpenGL-Rendering-System zu programmieren, möchte ich ein Level erstellen ein bisschen nach oben.
Frage:
Dort bin ich bisher und habe versucht zu untersuchen, wie andere Rendering-Engines mit ihren Materialsystemen umgehen.
Dieser knotenbasierte Ansatz ist großartig und scheint ein äußerst verbreitetes System zur Erstellung benutzerfreundlicher Materialsysteme in modernen Motoren und Werkzeugen zu sein. Soweit ich weiß, basieren sie auf einer Diagrammdatenstruktur, in der jeder Knoten einen Shader-Aspekt Ihres Materials darstellt und jeder Pfad eine Art Beziehung zwischen ihnen darstellt.
Soweit ich weiß, wäre die Implementierung eines solchen Systems eine so einfache MaterialNode-Klasse mit einer Vielzahl von Unterklassen (TextureNode, FloatNode, LerpNode usw.). Wo jede MaterialNode-Unterklasse MaterialConnections haben würde.
class MaterialConnection
{
MatNode_Out * fromNode;
MatNode_In * toNode;
}
class LerpNode : MaterialNode
{
MatNode_In x;
MatNode_In y;
MatNode_In alpha;
MatNode_Out result;
}
Das ist die Grundidee , aber ich bin mir ein bisschen unsicher, wie einige Aspekte dieses Systems funktionieren würden:
1.) Wenn Sie sich die verschiedenen 'Material Expressions' (Knoten) ansehen , die Unreal Engine 4 verwendet , werden Sie feststellen, dass sie jeweils über Eingangs- und Ausgangsverbindungen verschiedener Typen verfügen. Einige Knoten geben Floats aus, einige Ausgabevektoren2, einige Ausgabevektoren4 usw. Wie kann ich die obigen Knoten und Verbindungen verbessern, damit sie eine Vielzahl von Eingabe- und Ausgabetypen unterstützen können? Wäre es eine kluge Wahl, MatNode_Out mit MatNode_Out_Float und MatNode_Out_Vec4 (und so weiter) zu unterklassifizieren?
2.) Wie hängt diese Art von System mit GLSL-Shadern zusammen? Bei erneuter Betrachtung von UE4 (und ähnlich wie bei den anderen oben verlinkten Systemen) muss der Benutzer schließlich einen Materialknoten in einen großen Knoten mit verschiedenen Parametern einstecken, die Shader-Parameter darstellen (Grundfarbe, Metallizität, Glanz, Emissionsgrad usw.). . Meine ursprüngliche Annahme war, dass UE4 eine Art hartcodierten "Master-Shader" mit einer Vielzahl von Uniformen hatte und alles, was der Benutzer in seinem "Material" tut, einfach an den "Master-Shader" übergeben wird, wenn er seine Knoten in den "Master" einfügt. Hauptknoten '.
In der UE4-Dokumentation heißt es jedoch:
"Jeder Knoten enthält einen Ausschnitt aus HLSL-Code, der für die Ausführung einer bestimmten Aufgabe bestimmt ist. Dies bedeutet, dass Sie beim Erstellen eines Materials HLSL-Code durch visuelles Scripting erstellen."
Wenn das stimmt, generiert dieses System ein echtes Shader-Skript? Wie genau funktioniert das?
Antworten:
Ich werde versuchen, nach bestem Wissen und Gewissen zu antworten, mit wenig Wissen über den speziellen Fall von UE4, sondern über die allgemeine Technik.
Graphbasierte Materialien programmieren genauso viel wie das Schreiben des Codes selbst. Es fühlt sich einfach nicht so an für Leute ohne Code-Hintergrund, was es scheinbar einfacher macht. Wenn ein Designer einen "Hinzufügen" -Knoten verknüpft, schreibt er im Grunde add (Wert1, Wert2) und verknüpft die Ausgabe mit etwas anderem. Dies bedeutet, dass jeder Knoten HLSL-Code generiert, entweder als Funktionsaufruf oder nur als einfache Anweisungen.
Am Ende ist die Verwendung des Materialgraphen wie das Programmieren von Raw-Shadern mit einer Bibliothek vordefinierter Funktionen, die einige häufig verwendete nützliche Dinge ausführen, und genau das tut UE4 auch. Es verfügt über eine Bibliothek mit Shader-Code, die ein Shader-Compiler gegebenenfalls in die endgültige Shader-Quelle einfügt.
Im Fall von UE4, wenn sie behaupten, es sei in HLSL konvertiert, gehe ich davon aus, dass sie ein Konverter-Tool verwenden, das HLSL-Bytecode in GLSL-Bytecode konvertieren kann, sodass es auf GL-Plattformen verwendet werden kann. Andere Bibliotheken verfügen jedoch nur über mehrere Shader-Compiler, die das Diagramm lesen und direkt die erforderlichen Shading-Sprachquellen generieren.
Das Materialdiagramm ist auch eine gute Möglichkeit, von Plattformspezifikationen zu abstrahieren und sich auf das zu konzentrieren, was aus Sicht der Kunstrichtung wichtig ist. Da es nicht an eine Sprache und eine viel höhere Ebene gebunden ist, ist es einfacher, für die Zielplattform zu optimieren und anderen Code wie Light Handling dynamisch in den Shader einzufügen.
1) Um Ihre Fragen jetzt direkter beantworten zu können, sollten Sie einen datengesteuerten Ansatz für den Entwurf eines solchen Systems haben. Suchen Sie ein flaches Format, das in sehr einfachen Strukturen und sogar in einer Textdatei definiert werden kann. Im Wesentlichen sollte jeder Graph ein Array von Knoten mit einem Typ, einer Reihe von Ein- und Ausgängen sein, und jedes dieser Felder sollte eine lokale link_id haben, um sicherzustellen, dass die Graphverbindungen eindeutig sind. Außerdem kann jedes dieser Felder eine zusätzliche Konfiguration zu dem haben, was das Feld unterstützt (z. B. welcher Bereich von Datentypen unterstützt wird).
Mit diesem Ansatz können Sie das Feld eines Knotens einfach als (float | double) definieren und ihn den Typ aus den Verbindungen ableiten lassen oder einen Typ ohne Klassenhierarchien oder Überentwicklung in ihn zwingen. Es liegt an Ihnen, diese Diagrammdatenstruktur so starr oder flexibel zu gestalten, wie Sie möchten. Alles, was Sie brauchen, ist, dass es genügend Informationen enthält, damit der Codegenerator keine Mehrdeutigkeit aufweist und daher möglicherweise falsch behandelt, was Sie tun möchten. Wichtig ist, dass Sie auf der Ebene der Basisdatenstruktur flexibel bleiben und sich darauf konzentrieren, die Aufgabe zu lösen, ein Material allein zu definieren.
Wenn ich "Material definieren" sage, beziehe ich mich ganz speziell auf die Definition einer Netzoberfläche, die über das hinausgeht, was die Geometrie selbst bietet. Dies umfasst die Verwendung zusätzlicher Scheitelpunktattribute, um den Aspekt der Oberfläche zu konfigurieren, eine Verschiebung mit einer Höhenkarte hinzuzufügen, die Normalen mit Normalen pro Pixel zu stören, physikalisch basierte Parameter zu ändern, die BRDFs zu ändern und so weiter. Sie möchten nichts anderes wie HDR, Tonemapping, Skinning-Animation, Lichthandhabung oder viele andere Dinge beschreiben, die in Shadern ausgeführt werden.
2) Es ist dann Sache des Shader-Generators des Renderers, diese Datenstruktur zu durchlaufen und durch Lesen seiner Informationen eine Reihe von Variablen zusammenzustellen und diese mithilfe vorgefertigter Funktionen miteinander zu verknüpfen und den Code einzufügen, der die Beleuchtung und andere Effekte berechnet. Denken Sie daran, dass sich Shader nicht nur von verschiedenen Grafik-APIs unterscheiden, sondern auch zwischen verschiedenen Renderern (für ein verzögertes Rendern im Vergleich zu kachelbasierten Renderern und Forward-Renderern sind unterschiedliche Shader erforderlich). Mit einem solchen Materialsystem können Sie vom Bösen abstrahieren niedrige Ebene und konzentrieren sich nur auf die Beschreibung der Oberfläche
Für UE4 haben sie eine Liste mit Dingen für den von Ihnen erwähnten endgültigen Ausgabeknoten erstellt, die ihrer Meinung nach 99% der Oberflächen in alten und modernen Spielen beschreibt. Sie haben diesen Parametersatz über Jahrzehnte entwickelt und ihn mit der wahnsinnigen Menge an Spielen bewiesen, die die Unreal-Engine bisher produziert hat. Daher wird es Ihnen gut gehen, wenn Sie die Dinge genauso tun wie unwirklich.
Zum Abschluss schlage ich eine .material-Datei vor, um jedes Diagramm zu verarbeiten. Während der Entwicklung würde es möglicherweise ein textbasiertes Format zum Debuggen enthalten und dann zur Veröffentlichung in eine Binärdatei gepackt oder kompiliert werden. Jedes .material würde aus N Knoten und N Verbindungen bestehen, ähnlich wie eine SQL-Datenbank. Jeder Knoten hätte N Felder mit einem Namen und einigen Flags für akzeptierte Typen, wenn seine Eingabe oder Ausgabe, wenn die Typen abgeleitet werden usw. Die Laufzeitdatenstruktur für das geladene Material wäre genauso flach und einfach Der Editor kann es einfach anpassen und in der Datei speichern.
Und dann lassen Sie das eigentliche schwere Heben für die letzte Shader-Generation, was wirklich der schwierigste Teil ist. Das Schöne daran ist, dass Ihr Material für die Rendering-Plattform agnostisch bleibt. Theoretisch funktioniert es mit jeder Rendering-Technik und API, solange Sie das Material in der entsprechenden Schattierungssprache darstellen.
Lassen Sie mich wissen, wenn Sie zusätzliche Details oder Korrekturen in meiner Antwort benötigen. Ich habe den Überblick über den gesamten Text verloren.
quelle