Im ersten Teil unseres Artikels haben wir kurz vorgestellt, was die DCTL eigentlich ist und wie man DCTL-Scripte in Resolve einsetzen kann. Wen es danach in den Fingern gejuckt hat, einmal selber Hand anzulegen, dem helfen vielleicht die folgenden ersten Schritte beim Einstieg...
Ganz ohne Vorkenntnisse geht es allerdings nicht. Man sollte schon einmal etwas gescriptet haben und etwas von der C-Syntax mitbekommen haben. Darunter versteht man ein paar typische Regeln, wie man Script-Strukturen formuliert. Eine C-ähnliche Syntax kommt in zahlreichen Programmiersprachen (wie auch in Java oder Javascript) zum Einsatz. Funktionsblöcke werden beispielsweise in geschweiften Klammern zusammengefasst oder Befehle enden mit einem Strichpunkt.
Die erste Funktion
Die Haupteingabefunktion (transform) sollte als unterstes - also nach allen anderen Funktionen - im Programmtext definiert werden und laut Handbuch einen float3-Vektor zurückgeben. Dieser float3-Rückgabewert enthält als Ergebnis der Funktion die drei Farbkanäle des transformierten Pixels, also in der Regel Rot, Grün und Blau.
Schauen wir uns einfach einmal eine einfache DCTL mit einer einzigen (Haupteingabe)-Funktion an. Diese wird (wie Main() in anderen Programmiersprachen) als erstes vom Programm ausgeführt und muss wie jede Funktion in einem DCTL-Script mit "__DEVICE__" beginnen:
__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B)
{
const float r = p_R * 1.0f;
const float g = p_G * 2.0f;
const float b = p_B * 1.0f;
return make_float3(r, g, b);
}
Zum Ausprobieren speichert man dieses Script in einem Editor mit der Dateiendung *.DCTL ( also zum Beispiel "test.dctl" ) in folgendes Verzeichnis:
Mac OS X:
Bibliothek/Anwendungssupport/Blackmagic Design/DaVinci Resolve/LUT
Windows:
C:/ProgrammDaten/Blackmagic Design/DaVinci Resolve/Support/LUT
Linux:
/home/resolve/LUT
Wie man eine solche DCTL ausprobiert haben wir im ersten Teil dieses Artikels bereits erklärt. Sehen wir uns einmal detaillierter an, was in unserem Script passiert:
__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B)
In dieser ersten Zeile wird die Hauptfunktion (transform) definiert. Unser Script bekommt dabei ein paar Variablen von Resolve auf den Weg, die wir mit unserem Script im Anschluss nutzen können.
Dies sind Höhe und Breite des gesamten Frames (int p_Width, int p_Height), die exakte Position des zu bearbeitenden Pixels (int p_X, int p_Y) sowie dessen Rot, Grün und Blau-Wert (float p_R, float p_G, float p_B).
Int-Variablen fassen dabei ganzzahlige Werte und Float-Variablen sind Gleitkommawerte, die IMMER mit einem Punkt und NIE mit Komma getrennt werden. Damit man konstante Zahlenwerte im Script unterscheiden kann, hängt man bei Fließkommazahlen ein kleines f an die Zahl, um sie als float zu nutzen.
Im Funktionsblock schnappen wir uns die übergebenen Farbwerte des Eingangs-Pixels (p_R, p_G und p_B):
const float r = p_R * 1.0f;
const float g = p_G * 2.0f;
const float b = p_B * 1.0f;
Wir definieren also in einem Rutsch gleichzeitig drei neue lokale Variablen r, g und b in denen wir die Veränderungen durch unsere Funktion speichern wollen. Im unserem Beispiel multiplizieren wir einfach alle Pixel im grünen Kanal mit dem Faktor 2. Den roten und blauen Kanal lassen wir unverändert, indem wir mit 1 multiplizieren. Wir schreiben 1.0f, statt 1, damit der Computer weiß, dass wir mit Fließkommazahlen arbeiten.
Die letzte Zeile fasst die drei Variablen (r, g, b) zu einem dreidimensionalen Float-Vektor zusammen, den Resolve als Rückgabefunktion erwartet:
return make_float3(r, g, b);
RGB nach YUV
Kommen wir als nächstes gleich zu einem theoretischen Anwendungsfall: Wir wollen eine RGB nach YUV Transformation schreiben. Vielleicht ist der folgende Code schon für den einen oder anderes Leser selbsterklärend:
__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B)
{
const float y = 0.299f * p_R + 0.587f * p_G + 0.114f * p_B;
const float u = -0.147f * p_R - 0.289f * p_G + 0.436f * p_B;
const float v = 0.615f * p_R - 0.515f * p_G - 0.100f * p_B;
return make_float3(y, u, v);
}
Hier haben wir die typischen Konstanten der gängigsten YUV-Umrechungsformel genutzt, um diese Transformation als DCTL zu berechnen. In der Praxis sitzt man in diesem Fall jedoch auf einer YUV-Komponenten-Ausgabe, die man in Resolve mit weiteren DCTLs wieder wandeln müsste, weil die Vorschau ja RGB erwartet. Darum macht dieses Beispiel noch wenig Sinn. Aber es führt uns zu unserem praktischen Anwendungsfall:
Helligkeitsregler - hin und zurück
Wenn wir einen eigenen Helligkeitsregler erstellen wollen, müssen wir das Signal nach YUV wandeln, dann Y (also die Helligkeit) verändern und abschließend die gesamte Ausgabe wieder nach RGB wandeln. Auch das funktioniert jetzt relativ einfach, wenn man die Formeln der Wikipedia nutzt::
__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B)
{
const float y = (0.299f * p_R + 0.587f * p_G + 0.114f * p_B) * 1.0f;
const float u = -0.147f * p_R - 0.289f * p_G + 0.436f * p_B;
const float v = 0.615f * p_R - 0.515f * p_G - 0.100f * p_B;
const float r = 1.0f * y + 0.0f * u + 1.13983f * v;
const float g = 1.0f * y - 0.39465 *u - 0.58060 * v ;
const float b = 1.0f * y + 2.03211 * u + 0.0f * v;
return make_float3(r, g, b);
}
Dieses Beispiel scheint beim Ausprobieren erstmal gar nichts zu machen. Tatsächlich wandelt es jedoch jeden Pixel nach YUV, multipliziert den Y-Kanal mit 1.0f, wandelt danach alles wieder nach RGB und gibt das Ergebnis aus. Da man keine Veränderung sieht, scheinen die beiden Umrechnungsformeln also ok zu sein.
Dass tatsächlich in der Funktion von RGB nach YUV und zurück gewandelt wird, können wir feststellen, wenn wir den Skalierungsfaktor von Y ändern, also die Helligkeit im Y-Kanal verändern. Hierfür benutzen wir statt der 1.0f aus unserem Beispiel eine neue Variable, die wir "bright" nennen:
const float y = (0.299f * p_R + 0.587f * p_G + 0.114f * p_B) * bright;
Und diese Variable wollen wir durch einen Schieberegler verändern können. Dafür müssen wir den Schieberegler am Anfang unseres Scripts folgendermaßen definieren:
DEFINE_UI_PARAMS(bright, Helligkeit, DCTLUI_SLIDER_FLOAT, 1.0f, 0.0f, 10.0f, 0.01f)
Jetzt alles(s) zusammen...
In dieser Definition bestimmen wir den Variablennamen, der verändert werden soll (bright). Anschließend vergeben wir einen Namen für den Regler in Resolve (Helligkeit).
Die übrigen Werte definieren, dass es sich um einen Float-Regler handelt (DCTLUI_SLIDER_FLOAT), der beim Start den Wert 1.0f haben soll. Minimal soll der Regler den Wert 0.0f annehmen können, maximal den Wert 10.0f. Der letzte Wert beschreibt die Schrittweite, was jedoch nur bei Int-Reglern sinnvoll funktioniert. Unser gesamtes Script zur Veränderung der Helligkeit sieht schließlich so aus:
DEFINE_UI_PARAMS(bright, Helligkeit, DCTLUI_SLIDER_FLOAT, 1.0f, 0.0f, 10.0f, 0.01f)
__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B)
{
const float y = (0.299f * p_R + 0.587f * p_G + 0.114f * p_B) * bright;
const float u = -0.147f * p_R - 0.289f * p_G + 0.436f * p_B;
const float v = 0.615f * p_R - 0.515f * p_G - 0.100f * p_B;
const float r = 1.0f * y + 0.0f * u + 1.13983f * v;
const float g = 1.0f * y - 0.39465 *u - 0.58060 * v ;
const float b = 1.0f * y + 2.03211 * u + 0.0f * v;
return make_float3(r, g, b);
}

Das wars erst einmal in aller Kürze und Würze. Mit diesem Wissen könnte man nun schon beispielsweise Probleme zur individuellen Farbraumsättigung angehen, die wir vor einem Jahr auf slashCAM angesprochen hatten. Aber natürlich gibt es auch noch weitaus komplexere Anwendungsfälle. Wer hier Anregungen sucht, sollte unbedingt die OFX-DCTLs von Baldavenger auf Guthub studieren.
Wer nun grundsätzlich Shader-Blut geleckt hat, kann sich auch einmal dieses Einsteiger-Video zu gemüte führen, welches an einem relativ einfachen Beispiel durchgeht, wie man mit Shadern prozedural gestalten kann:
Und wer rund sechs Stunden Zeit entbehren kann, darf im folgenden Video die schier unglaubliche Macht der Shader Programmierung in der Praxis verfolgen (und lernen):
Dort zeigt der Macher Schritt für Schritt, wie er das Shader-Meisterwerk "Happy Jumping" konstruiert hat.
Natürlich konnten wir in diesem Artikel wirklich nur das wesentlichste für die ersten Schritte der DCTL-Programmierung anreißen. Sollte es ein stärkeres Feedback geben können wir diesen Artikel gerne noch detaillierter ausbauen. Wir hätten druchaus noch einige Tricks zu teilen, denn wir haben unter anderem unsere redaktionellen Rolling Shutter Messungen selbst mittels DCTLs implementiert.