In dem Artikel über Blending-Modes habe ich teilweise auch Blending-Modes beschrieben bzw. Ergebnisse von Blending-Modes gezeigt, die nicht Teil von Substance 3D Designer sind. Vielleicht hat sich ja der ein oder andere von euch gefragt, wie ich das gemacht habe. Die Antwort lautet: “Mit dem Pixel-Prozessor!” (Eine andere Antwort wäre bei dieser Artikelüberschrift vermutlich auch verwirrend gewesen 😉) Und wie ich das genau gemacht habe, möchte ich in diesem Artikel zeigen.
Was ist der Pixel-Prozessor?

Neuer Pixel-Prozessor
Der Pixel-Prozessor ist neben FxMap einer der mächtigstens Knoten von Substance 3D Designer, auch wenn er beim ersten Kontakt eher unnütz erscheint. Das liegt daran, dass der Pixel-Prozessor von sich aus genau gar nichts macht und erst einmal nur ein schwarzes Bild als Ergebnis liefert, unabhängig von der Eingabe. Die Stärke des Pixel-Prozessors eröffnet sich einem dann, wenn man die “Per Pixel Function” editiert und mit Leben füllt. Dafür klickt man entweder unterhalb von “Per Pixel Function” auf das “Edit function”, welches man unter den “Specific Parameters” des Pixel-Prozessors findet oder man drückt einfach Strg + E
.

Leerer Funktionsgraph
Begrüßt wird man dann mit einem neuen leeren Graphen. Wenn man sich die zur Verfügung stehenden Knoten anschaut, wird man, sofern man keine Erfahrung mit Shadern hat, zweifelsohne kurz an den Matheunterricht denken müssen. Aber erst einmal alles halb so wild. Wie der Name des Parameters verrät, wird dieser Graph bzw. diese Funktion für jeden einzelnen Pixel des Eingabebilds aufgerufen, also bei einem 1024x1024 Bild gut 1 Million mal, und ist somit ein wenig mit einem Fragment-/ Pixel-Shader vergleichbar.

Erwarteter Ausgabewert (untere linke Ecke des Funktionsgraphs)
Unten links im Fenster steht, was der Graph als Ausgabe erwartet. Im Falle des Pixel-Prozessors ist es entweder ein Float
oder ein Float4
, also entweder eine Graustufen- oder ein RGBA-Wert.
Einfache Beispiele
Für den Einstieg habe ich mir ein paar einfache Beispiele überlegt. Sie sind so gewählt, dass wir uns in jedem Beispiel etwas steigern, ohne dass man gleich Angst vor komplexen Formeln bekommen muss.
Der wohl einfachste Pixel-Prozessor

Einfachste Pixel-Prozessor-Funktion
Die wohl einfachste Art, die “Per Pixel Function” mit Leben zu füllen, ist es, eine statische Ausgabe zu machen, indem wir jeden Pixel bspw. hellgrau einfärben. Dafür erzeugen wir einen “Float”-Knoten, geben diesem den Wert \(0.8\) und erklären diesen Knoten zum Ausgabeknoten, indem wir im Kontextmenü des Knotens (Rechtsklick) “Set as Output Node” auswählen. Wenn das geklappt hat, ändert sich die Hintergrundfarbe des Knotens in Orange. Somit geben wir für jeden Aufruf, also Pixel, den Wert \(0.8\) zurück, der bei Substance 3D Designer (Wertebereich 0 - 1) bei Graustufen für ein Hellgrau steht.

Ergebnis der einfachsten Pixel-Prozessor-Funktion
Der erste eigene lineare Gradient

Erzeugung eines vertikalen Gradienten im Pixel-Prozessor
Nachdem wir nun wissen, wie wir jedem Pixel den gleichen Wert zuweisen, machen wir es doch etwas flexibler und erzeugen einen linearen Gradienten. Wir müssen nun also wissen, welchen Pixel wir gerade erstellen. Dafür gibt es eine Float2
Variable namens $pos
, die wir mit Hilfe des “Get Float2”-Knotens auslesen können, indem wir im Dropdown-Feld die Variable direkt auswählen oder selbst hineinschreiben. Die X- und Y-Werte der Position reichen ebenfalls von 0 bis 1, wobei Pixel (0, 0) immer die Ecke links oben und Pixel (1, 1) die Ecke rechts unten bezeichnen. Für einen horizontalen Gradienten benötigen wir also unsere X-Position und für die vertikale Version entsprechend unsere Y-Position. Um einen Vektor aufzuteilen, gibt es diverse “Swizzle”-Knoten, je nachdem, wie viele Werte man am Ende haben möchte. Wir möchten nur einen Wert haben, also nehmen wir den Knoten “Swizzle Float 1” und wählen je nach gewünschtem Ergebnis entweder X oder Y aus. Anschließend erklären wir den “Swizzle Float 1”-Knoten wieder zu unserem Ausgabeknoten und schon haben wir einen linearen Gradienten 🎉

Erzeugter vertikaler Gradient
Durchschleifen eines Eingabebildes

Durchschleifen eines Eingabebildes
Als letzte Grundlage müssen wir uns noch anschauen, wie wir auf Eingabebilder zugreifen. Die aktuelle Pixelposition $pos
ist hier natürlich ebenfalls essentiell, aber zusätzlich benötigen wir noch einen Sample
Knoten. Es gibt zwei Arten von Sample
Knoten, Sample Color
und Sample Gray
, die ihren Namen entsprechend entweder RGBA- oder Graustufenwerte liefern. Als Eingabe erwarten sie jeweils ein Float2
, also die gewünschte Pixelposition. Um nun einfach ein Bild unverändert auszugeben, also durchzuschleifen, verbinden wir unsere aktuelle Pixelposition mit dem Sample
Knoten unserer Wahl und erklären den Sample
Knoten zum Ausgabeknoten. Fertig ist das Durchschleifen eines Eingabebildes.

Pixel-Prozessor in der Anwendung
Nachbau von Blending-Modes
Nachdem wir nun die Grundlagen verstanden haben, widmen wir uns Aufgaben, deren Ergebnis sich leicht nachprüfen lässt. Wir bauen bereits in Substance 3D Designer integrierte Blending-Modes nach und können uns selbst überprüfen, indem wir unser Ergebnis mit der integrierten Version vergleichen.
Add
Fangen wir wieder mit einem einfachen Blending-Mode an: Add.
Zur Erinnerung, die Formel war: \(p_\text{new} = p_\text{bg} + p_\text{fg}\)

Add-Funktion
Wir erstellen einen neuen Pixel-Prozessor und versorgen ihn mit den gewünschten Eingabedaten. Wir orientieren uns dabei am integrierten Blend-Knoten, sodass der obere Eingang das Vordergrundbild und der Eingang darunter das Hintergrundbild sein wird.
An die aktuellen Pixel gelangen wir über Sample
Knoten. In den Optionen der Sample
Knoten lässt sich konfigurieren, welches Eingabebild genommen werden soll. Das erste Bild (oberster Eingang) ist “Input Image 0”, das zweite Bild entsprechend “Input Image 1”. Als Eingabewert beider Sample
Knoten nutzen wir wie gewohnt unsere aktuelle Pixelposition $pos
. Als letzten Schritt müssen wir nun beide Werte noch addieren, was mit dem “Add”-Knoten geschieht, welcher dann auch zum Ausgabeknoten erklärt wird.

Pixel-Prozessor in der Anwendung inkl. Vergleich mit der Originallösung
Soft Light
Erhöhen wir den Schwierigkeitsgrad etwas, indem wir nun einen komplexeren Blending-Mode nachbauen: Soft Light.
Zur Erinnerung, die Formel der integrierten Version war:
\( p_\text{new} = \begin{cases} 2 p_\text{bg} p_\text{fg} + p_\text{bg}^2 (1 - 2 p_\text{fg}) & p_\text{fg} \lt 0.5 \\ \sqrt{p_\text{bg}} (2 p_\text{fg} - 1) + 2 p_\text{bg} (1 - p_\text{fg}) & \, \text{sonst} \end{cases} \)

Soft-Light-Funktion
Die Formel mag im Vergleich zu den bisherigen Beispielen sehr komplex sein, aber wenn wir sie einfach Schritt für Schritt nachbauen, ist es nicht viel schwerer als in den bisherigen Beispielen, nur länger 😉
Der Grundgraph sieht genauso aus, wie bei unserem Add. Wir benötigen unsere aktuelle Pixelposition $pos
und zwei Sample
Knoten, also für jedes Eingabebild einen.

Soft-Light-Funktion (Grundaufbau)
Fangen wir anschließend mit dem ersten Teilterm an: \(2 * p_\text{bg} * p_\text{fg}\). Wir benötigen also einen Float
Knoten mit dem Wert 2
und zwei Multiply
Knoten, die wir mit unseren Sample
Knoten verbinden.

Soft-Light-Funktion (1. Teilterm)
Der zweite Teilterm lautet \(p_\text{bg}^2 (1 - 2 p_\text{fg})\). Wer beim vorigen Teilterm genau aufgepasst hat, merkt, dass ich die Reihenfolge der Multiplikationen verändert habe. So können wir ein Zwischenergebnis auch für diesen Teilterm nutzen. Für das Quadrieren existiert der Pow
Knoten und für die Subtraktion von 1 existiert ein One Minus
Knoten, sodass wir nun nur noch alles passend verbinden müssen. Anschließend addieren wir unsere beiden Teilterme, sodass wir nun den ersten Fall bereits nachprogrammiert haben.

Soft-Light-Funktion (1. Fall)
Beim zweiten Fall gehen wir ebenfalls Stück für Stück vor und teilen den gesamten Term in die beiden Summanden auf. Wir fangen also mit \(\sqrt{p_\text{bg}} (2 p_\text{fg} - 1)\) an. Die Quadratwurzel können wir mit Hilfe des Square Root
Knotens ziehen. Subtrahieren geht natürlich mit dem Subtraction
Knoten. Auch hier können wir unser Zwischenergebnis \(2 * p_\text{fg}\) aus dem ersten Teilterm wiederverwenden.

Soft-Light-Funktion (2. Fall, 1. Teilterm)
Kommen wir nun zum letzten Teilterm \(2 * p_\text{bg} * (1 - p_\text{fg})\). Alle benötigten Knoten haben wir bereits kennengelernt, sodass das Ergebnis keine Überraschung sein sollte.

Soft-Light-Funktion (2. Fall)
Last, but not least müssen wir nun noch die Fallunterscheidung umsetzen. Dafür gibt es natürlich ebenfalls einen passenden Knoten, den If…Else
Knoten. Der If…Else
Knoten hat die drei Eingänge Condition
, If
und Else
. Wenn Condition
zutrifft, wird die Eingabe von If
weitergegeben und ansonsten die von Else
. Für den Vergleich benötigen wir noch den Lower
Knoten und dann geht’s ans Verdrahten. Zum Schluss erklären wir noch den If…Else
Knoten zum Ausgabeknoten und fertig ist die eigene Implementierung von Soft Light.

Soft-Light-Funktion

Pixel-Prozessor in der Anwendung inkl. Vergleich mit der Originallösung
Erstellen eigener Funktionen

Eingabeparameter Opacity-Funktion
Im letzten Teil dieses Artikels möchte ich nun noch die Deckkraft integrieren. Wie wir aus meinem letzten Artikel wissen, ist die Deckkraft unabhängig vom Blending-Mode. Wir können uns also einmal die Mühe machen, sie zu implementieren, sodass wir sie anschließend in jedem Blending-Mode nutzen können. Damit wir das nicht immer per Copy’n’Paste machen müssen, benutzen wir dafür eine Funktion. Die Funktion erstellen wir, indem wir in unserem aktuellen “Substance Package” einen neuen “Substance function graph” anlegen und passenderweise “Opacity” nennen.
Für die Implementierung greifen wir auf die bekannte Formel aus dem letzten Artikel zurück: \(p_\text{opa} = O * p_\text{new} + (1 - O) * p_\text{bg}\)
Wir haben also drei Eingabewerte, die wir miteinander verrechnen müssen, alles sind Float
Werte. Damit wir überhaupt Zugriff auf Eingabewerte haben, müssen wir diese bei unserer Funktion deklarieren, was, wie bei Substance 3D Designer üblich, innerhalb der “Parameters” erfolgt. Wir legen also drei Float
Werte an und nennen sie opacity
, fg
und bg
.
Da alle Eingabewerte Float
Werte sind, können wir dreimal den Get Float
Knoten nutzen. Als Namen des jeweils auszulesenden Wertes müssen wir nun den Namen des passenden Eingabeparameters wählen bzw. eingeben. Alle weiteren Operationen sollten ebenfalls bekannt sein, sodass wir schnell zwei Multiplication
Knoten, einen One Minus
Knoten und einen Add
Knoten erstellen und dann die Formel verdrahten. Zum Schluss erklären wir unseren Add
Knoten noch zum Ausgabeknoten und das Programmieren der Funktion ist abgeschlossen.

Opacity-Funktion
Zum Schluss können wir unsere Funktion nun noch in unsere bereits umgesetzten Blending-Modes integrieren. Der erste Schritt dafür ist es, unserem Pixel Prozessor
Knoten einen neuen Eingabeparameter #opacity
zu geben. Im nächsten Schritt gehen wir in die jeweilige Per Pixel Function
und ziehen unsere frisch programmierte Funktion Opacity
hinein. Wir erhalten also einen Knoten mit drei Eingabeparametern und einem Ausgabeparameter, welcher nun zu unserem neuen Ausgabeknoten erklärt wird. Damit er seine Aufgabe verrichten kann, müssen wir ihn nur noch richtig integrieren. Als fg
nutzen wir den bisherigen Ausgabeknoten, als bg
den Sample
Knoten des Eingabehintergrunds und für opacity
müssen wir noch unseren neu erzeugten Parameter auslesen und weiterreichen. Alles in allem sollten die Funktionsgraphen dann in etwa so aussehen.
Setup für die Nutzung der Opacity-Funktion
Add-Modus mit Opacity-Funktion
Fazit
Ich hoffe, diese Einführung war für euch mindestens so interessant und spannend wie für mich die Konzeption und Erstellung des Artikels. Ich hoffe, dass ich euch einen guten ersten Eindruck von der möglichen Mächtigkeit des Pixel-Prozessors liefern konnte, denn was ich hier bislang gezeigt habe, waren nur einfache Beispiele. Mit den Grundlagen ist es euch nun möglich, jeden beliebigen Algorithmus auf Pixelbasis zu implementieren und so Knoten zu erstellen, die genau das machen, was ihr von ihnen gerne hättet.