Programmierbare Materialien, Effekte, Geometrie und Texturdaten

Während die eingebauten Materialien von Qt Quick 3D, DefaultMaterial und PrincipledMaterial ein hohes Maß an Anpassung über ihre Eigenschaften erlauben, bieten sie keine Programmierbarkeit auf der Vertex- und Fragment-Shader-Ebene. Um dies zu ermöglichen, wird der Typ CustomMaterial bereitgestellt.

Ein Modell mit PrincipledMaterialMit einer CustomMaterial, die die Scheitelpunkte transformiert

Nachbearbeitungseffekte, bei denen ein oder mehrere Durchgänge der Bearbeitung des Farbpuffers durchgeführt werden, optional unter Berücksichtigung des Tiefenpuffers, bevor die Ausgabe von View3D an Qt Quick weitergegeben wird, gibt es ebenfalls in zwei Varianten:

  • eingebaute Nachbearbeitungsschritte, die über ExtendedSceneEnvironment konfiguriert werden können, wie z. B. Glühen/Blühen, Tiefenschärfe, Vignette, Streulicht,
  • custom Effekte, die von der Anwendung in Form von Fragment-Shader-Code implementiert werden, und eine Spezifikation der Verarbeitungsschritte in einem Effect Objekt.

In der Praxis gibt es eine dritte Kategorie von Nachbearbeitungseffekten: 2D-Effekte, die über Qt Quick implementiert werden und auf die Ausgabe des View3D Objekts ohne jegliche Beteiligung des 3D-Renderers wirken. Um zum Beispiel einen Weichzeichner auf ein View3D Objekt anzuwenden, ist es am einfachsten, die bestehenden Möglichkeiten von Qt Quick zu nutzen, wie zum Beispiel MultiEffect. Das 3D-Nachbearbeitungssystem ist von Vorteil für komplexe Effekte, die 3D-Szenenkonzepte wie den Tiefenpuffer oder die Bildschirmtextur einbeziehen, mit HDR-Tonemapping umgehen müssen oder mehrere Durchläufe mit Zwischenpuffern erfordern usw. Einfache 2D-Effekte, die keinen Einblick in die 3D-Szene und den Renderer erfordern, können stattdessen immer mit ShaderEffect oder MultiEffect implementiert werden.

Szene ohne EffektDie gleiche Szene mit einem benutzerdefinierten Post-Processing-Effekt

Zusätzlich zu den programmierbaren Materialien und der Nachbearbeitung gibt es zwei Arten von Daten, die normalerweise in Form von Dateien (.mesh Dateien oder Bilder wie .png) bereitgestellt werden:

  • Vertex-Daten, einschließlich der Geometrie für das zu rendernde Mesh, Texturkoordinaten, Normalen, Farben und andere Daten,
  • den Inhalt für Texturen, die dann als Textur-Maps für die gerenderten Objekte oder mit Skybox oder bildbasierter Beleuchtung verwendet werden.

Wenn sie es wünschen, können Anwendungen diese Daten von C++ aus in Form einer QByteArray bereitstellen. Diese Daten können auch im Laufe der Zeit geändert werden, so dass die Daten für eine Model oder Texture prozedural erzeugt und später geändert werden können.

Ein Gitter, gerendert durch die dynamische Angabe von Vertex-Daten aus C++Ein Würfel, der mit aus C++ generierten Bilddaten texturiert wird

Diese vier Ansätze zur Anpassung und Dynamisierung von Materialien, Effekten, Geometrie und Texturen ermöglichen die Programmierbarkeit des Shadings und die prozedurale Erzeugung der Daten, die die Shader als Eingabe erhalten. Die folgenden Abschnitte geben einen Überblick über diese Funktionen. Die vollständige Referenz finden Sie auf den Dokumentationsseiten für die jeweiligen Typen:

Programmierbarkeit für Materialien

Nehmen wir eine Szene mit einem Würfel und beginnen wir mit einem Standard PrincipledMaterial und CustomMaterial:

PrincipledMaterialCustomMaterial
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Cube"
            scale: Qt.vector3d(2, 2, 2)
            eulerRotation.x: 30
            materials: PrincipledMaterial { }
         }
    }
}
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Cube"
            scale: Qt.vector3d(2, 2, 2)
            eulerRotation.x: 30
            materials: CustomMaterial { }
         }
    }
}

Beide führen zu genau demselben Ergebnis, da ein CustomMaterial effektiv ein PrincipledMaterial ist, wenn kein Vertex- oder Fragment-Shader-Code hinzugefügt wird.

Hinweis: Eigenschaften wie baseColor, metalness, baseColorMap und viele andere haben keine entsprechenden Eigenschaften im CustomMaterial QML-Typ. Das ist so gewollt: Die Anpassung des Materials erfolgt über den Shader-Code und nicht durch die bloße Bereitstellung einiger fester Werte.

Unser erster Vertex-Shader

Fügen wir nun ein benutzerdefiniertes Vertex-Shader-Snippet hinzu. Dazu verweisen wir auf eine Datei in der Eigenschaft vertexShader. Der Ansatz ist derselbe für Fragment-Shader. Diese Verweise funktionieren wie Image.source oder ShaderEffect.vertexShader: Sie sind lokale oder qrc URLs, und ein relativer Pfad wird relativ zum Speicherort der .qml Datei behandelt. Der übliche Ansatz besteht daher darin, die Dateien .vert und .frag im Qt-Ressourcensystem zu platzieren (qt_add_resources bei Verwendung von CMake) und sie über einen relativen Pfad zu referenzieren.

In Qt 6.0 werden Inline-Shader-Strings nicht mehr unterstützt, weder in Qt Quick noch in Qt Quick 3D(beachten Sie die Tatsache, dass diese Eigenschaften URLs und keine Strings sind). Aufgrund ihrer inhärent dynamischen Natur stellen benutzerdefinierte Materialien und Post-Processing-Effekte in Qt Quick 3D jedoch immer noch Shader-Snippets in Quellform in den referenzierten Dateien bereit. Dies ist ein Unterschied zu ShaderEffect, wo die Shader vollständig sind, ohne weitere Änderungen durch die Engine, und daher erwartet wird, dass sie als vorbereitete .qsb Shader-Pakete bereitgestellt werden.

Hinweis: In Qt Quick 3D können URLs nur auf lokale Ressourcen verweisen. Schemata für entfernte Inhalte werden nicht unterstützt.

Hinweis: Die verwendete Shading-Sprache ist Vulkan-kompatibles GLSL. Die Dateien .vert und .frag sind für sich genommen keine vollständigen Shader, weshalb sie oft als snippets bezeichnet werden. Aus diesem Grund gibt es keine einheitlichen Blöcke, Eingabe- und Ausgabevariablen oder Sampler-Uniformen, die direkt von diesen Snippets bereitgestellt werden. Stattdessen werden sie von der Qt Quick 3D -Engine nach Bedarf angepasst.

Änderung in main.qml, material.vertErgebnis
materials: CustomMaterial {
    vertexShader: "material.vert"
}
void MAIN()
{
}

Von einem benutzerdefinierten Vertex- oder Fragment-Shader-Snippet wird erwartet, dass es eine oder mehrere Funktionen mit vordefinierten Namen bereitstellt, wie MAIN, DIRECTIONAL_LIGHT, POINT_LIGHT, SPOT_LIGHT, AMBIENT_LIGHT, SPECULAR_LIGHT. Konzentrieren wir uns zunächst auf MAIN.

Wie hier gezeigt wird, ist das Endergebnis mit einem leeren MAIN() genau dasselbe wie zuvor.

Bevor wir es interessanter machen, wollen wir uns einen Überblick über die am häufigsten verwendeten speziellen Schlüsselwörter in benutzerdefinierten Vertex-Shader-Snippets verschaffen. Dies ist nicht die vollständige Liste. Eine vollständige Referenz finden Sie auf der Seite CustomMaterial.

SchlüsselwortTypBeschreibung
MAINvoid MAIN() ist der Einstiegspunkt. Diese Funktion muss immer in einem benutzerdefinierten Vertex-Shader-Snippet vorhanden sein, andernfalls hat es keinen Sinn, eine Funktion bereitzustellen.
VERTEXvec3Die Vertex-Position, die der Shader als Eingabe erhält. Ein häufiger Anwendungsfall für Vertex-Shader in benutzerdefinierten Materialien ist das Ändern (Verschieben) der x-, y- oder z-Werte dieses Vektors, indem einfach dem gesamten Vektor oder einigen seiner Komponenten ein Wert zugewiesen wird.
NORMALvec3Die Scheitelnormalen aus den Eingabedaten des Netzes, oder alle Nullen, wenn keine Normalen angegeben wurden. Wie bei VERTEX steht es dem Shader frei, den Wert nach eigenem Ermessen zu ändern. Der geänderte Wert wird dann vom Rest der Pipeline verwendet, einschließlich der Beleuchtungsberechnungen in der Fragmentstufe.
UV0vec2Der erste Satz von Texturkoordinaten aus den eingegebenen Mesh-Daten oder alle Nullen, wenn keine UV-Werte angegeben wurden. Wie bei VERTEX und NORMAL kann der Wert geändert werden.
MODELVIEW_PROJECTION_MATRIXMatte4Die Modell-Ansicht-Projektionsmatrix. Um das Verhalten zu vereinheitlichen, unabhängig davon, mit welcher Grafik-API das Rendering erfolgt, folgen alle Vertex-Daten und Transformationsmatrizen auf dieser Ebene den OpenGL-Konventionen. (Y-Achse zeigt nach oben, OpenGL-kompatible Projektionsmatrix) Nur lesen.
MODELL_MATRIXMatte4Die Modell-(Welt-)Matrix. Nur lesbar.
NORMAL_MATRIXmat3Die transponierte Umkehrung des linken oberen 3x3-Slice der Modellmatrix. Nur lesbar.
KAMERA_LAGEvec3Die Kameraposition im Weltraum. In den Beispielen auf dieser Seite ist dies (0, 0, 600). Nur lesen.
CAMERA_DIRECTIONvec3Der Richtungsvektor der Kamera. In den Beispielen auf dieser Seite ist dies (0, 0, -1). Nur lesen.
CAMERA_PROPERTIESvec2Die Nah- und Fern-Clip-Werte der Kamera. In den Beispielen auf dieser Seite ist dies (10, 10000). Kann nur gelesen werden.
PUNKT_GRÖSSEFloatNur relevant, wenn mit einer Topologie aus Punkten gerendert wird, z. B. weil die custom geometry eine solche Geometrie für das Netz bereitstellt. Das Schreiben dieses Wertes ist gleichbedeutend mit dem Setzen von pointSize on a PrincipledMaterial.
POSITIONvec4Wie gl_Position. Wenn nicht vorhanden, wird automatisch eine Standard-Zuweisungsanweisung unter Verwendung von MODELVIEWPROJECTION_MATRIX und VERTEX generiert. Aus diesem Grund ist ein leeres MAIN() funktional, und in den meisten Fällen wird es nicht nötig sein, ihm einen eigenen Wert zuzuweisen.

Lassen Sie uns ein benutzerdefiniertes Material erstellen, das die Scheitelpunkte nach einem bestimmten Muster verschiebt. Um es interessanter zu machen, haben wir einige animierte QML-Eigenschaften, deren Werte am Ende als Uniformen im Shader-Code sichtbar werden. (um genau zu sein, werden die meisten Eigenschaften auf Mitglieder in einem Uniform-Block abgebildet, der zur Laufzeit durch einen Uniform-Puffer unterstützt wird, aber Qt Quick 3D macht solche Details für den Autor des benutzerdefinierten Materials transparent)

Änderung in main.qml, material.vertErgebnis
materials: CustomMaterial {
   vertexShader: "material.vert"
   property real uAmplitude: 0
   NumberAnimation on uAmplitude {
       from: 0; to: 100; duration: 5000; loops: -1
   }
   property real uTime: 0
   NumberAnimation on uTime {
       from: 0; to: 100; duration: 10000; loops: -1
   }
}
void MAIN()
{
    VERTEX.x += sin(uTime + VERTEX.y) * uAmplitude;
}

Uniformen aus QML-Eigenschaften

Benutzerdefinierte Eigenschaften im CustomMaterial Objekt werden auf Uniformen abgebildet. Im obigen Beispiel sind dies uAmplitude und uTime. Jedes Mal, wenn sich die Werte ändern, wird der aktualisierte Wert im Shader sichtbar. Dieses Konzept ist vielleicht schon von ShaderEffect bekannt.

Der Name der QML-Eigenschaft und der GLSL-Variable müssen übereinstimmen. Es gibt keine separate Deklaration im Shader-Code für die einzelnen Uniformen. Vielmehr kann der Name der QML-Eigenschaft so verwendet werden, wie er ist. Aus diesem Grund kann im obigen Beispiel im Vertex-Shader-Snippet einfach auf uTime und uAmplitude verwiesen werden, ohne dass eine vorherige Deklaration dafür erforderlich ist.

In der folgenden Tabelle ist aufgeführt, wie die Typen zugeordnet werden:

QML-TypShader-TypHinweise
real, int, boolfloat, int, bool
Farbevec4sRGB zu linear Konvertierung wird implizit durchgeführt
vector2dvec2
vector3dvec3
vector4dvec4
matrix4x4matte4
Quaternionvec4skalarer Wert ist w
rektvec4
Punkt, Größevec2
TextureInputProbenehmer2D

Verbessern des Beispiels

Bevor wir weitermachen, wollen wir das Beispiel noch etwas verbessern. Durch das Hinzufügen eines gedrehten Rechteckmeshes und die Erzeugung von Schatten auf DirectionalLight können wir überprüfen, ob die Änderung der Würfelpunkte in allen Rendering-Passes, einschließlich der Schattenkarten, korrekt wiedergegeben wird. Um einen sichtbaren Schatten zu erhalten, wird das Licht nun etwas höher auf der Y-Achse platziert und eine Drehung angewendet, damit es teilweise nach unten zeigt. (da es sich um ein directional Licht handelt, ist die Rotation wichtig)

main.qml, material.vertErgebnis
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color; clearColor: "black" }
        PerspectiveCamera { z: 600 }
        DirectionalLight {
            y: 200
            eulerRotation.x: -45
            castsShadow: true
        }
        Model {
            source: "#Rectangle"
            y: -250
            scale: Qt.vector3d(5, 5, 5)
            eulerRotation.x: -45
            materials: PrincipledMaterial { baseColor: "lightBlue" }
        }
        Model {
            source: "#Cube"
            scale: Qt.vector3d(2, 2, 2)
            eulerRotation.x: 30
            materials: CustomMaterial {
                vertexShader: "material.vert"
                property real uAmplitude: 0
                NumberAnimation on uAmplitude {
                    from: 0; to: 100; duration: 5000; loops: -1
                }
                property real uTime: 0
                NumberAnimation on uTime {
                    from: 0; to: 100; duration: 10000; loops: -1
                }
            }
        }
    }
}
void MAIN()
{
    VERTEX.x += sin(uTime + VERTEX.y) * uAmplitude;
}

Hinzufügen eines Fragment-Shaders

Viele benutzerdefinierte Materialien werden auch einen Fragment-Shader haben wollen. In der Tat werden viele nur einen Fragment-Shader wollen. Wenn keine zusätzlichen Daten von der Vertex- zur Fragmentstufe übergeben werden müssen und die standardmäßige Vertex-Transformation ausreicht, kann die Einstellung der Eigenschaft vertexShader auf CustomMaterial entfallen.

Änderung in main.qml, material.fragErgebnis
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
}

Unser erster Fragment-Shader enthält eine leere MAIN()-Funktion. Das ist nicht anders, als wenn wir überhaupt kein Fragment-Shader-Snippet angeben: Was wir erhalten, sieht genauso aus wie das, was wir mit einem Standard PrincipledMaterial erhalten.

Schauen wir uns einige der häufig verwendeten Schlüsselwörter in Fragment-Shadern an. Dies ist nicht die vollständige Liste, lesen Sie die CustomMaterial Dokumentation für eine vollständige Referenz. Viele dieser Schlüsselwörter sind schreibgeschützt, d. h. sie haben einen Standardwert, aber der Shader kann ihnen einen anderen Wert zuweisen und wird dies oft auch tun.

Wie die Namen vermuten lassen, entsprechen viele dieser Eigenschaften ähnlich benannten PrincipledMaterial Eigenschaften mit derselben Bedeutung und Semantik, die dem Metallic-Roughness-Materialmodell folgen. Es ist Sache der benutzerdefinierten Materialimplementierung zu entscheiden, wie diese Werte berechnet werden: Ein Wert für BASE_COLOR kann beispielsweise im Shader fest kodiert sein, auf dem Sampling einer Textur basieren oder auf der Grundlage von QML-Eigenschaften berechnet werden, die als Uniformen oder interpolierte Daten vom Vertex-Shader weitergegeben werden.

SchlüsselwortTypBeschreibung
BASE_COLORvec4Die Grundfarbe und der Alphawert. Entspricht PrincipledMaterial::baseColor. Der endgültige Alphawert des Fragments ist die Modell-Deckkraft multipliziert mit der Grundfarbe Alpha. Der Standardwert ist (1.0, 1.0, 1.0, 1.0).
EMISSIVE_COLORvec3Die Farbe der Selbstillumination. Entspricht PrincipledMaterial::emissiveFactor. Der Standardwert ist (0.0, 0.0, 0.0).
METALNESSFloatMetalness Wert im Bereich 0-1. Standardwert 0, was bedeutet, dass das Material dielektrisch (nicht metallisch) ist.
ROUGHNESSFließkommaRoughness Wert im Bereich von 0-1. Der Standardwert ist 0. Größere Werte machen spiegelnde Glanzlichter weicher und verwischen Reflexionen.
SPIEGELNDER_BETRAGFließkommazahlThe strength of specularity im Bereich 0-1. Der Standardwert ist 0.5. Bei metallischen Objekten, bei denen metalness auf 1 eingestellt ist, hat dieser Wert keine Auswirkungen. Wenn sowohl SPECULAR_AMOUNT als auch METALNESS Werte größer als 0, aber kleiner als 1 haben, ist das Ergebnis eine Mischung zwischen den beiden Materialmodellen.
NORMALvec3Die interpolierte Normale im Weltraum, angepasst für Doppelseitigkeit, wenn Face Culling deaktiviert ist. Nur lesen.
UV0vec2Die interpolierten Texturkoordinaten. Nur Lesen.
VAR_WELT_POSITIONvec3Interpolierte Scheitelpunktposition im Weltraum. Nur lesen.

Färben wir die Grundfarbe des Würfels rot:

Änderung in main.qml, material.fragErgebnis
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 1.0);
}

Jetzt verstärken wir den Grad der Selbstbeleuchtung ein wenig:

Änderung in main.qml, material.fragErgebnis
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 1.0);
    EMISSIVE_COLOR = vec3(0.4);
}

Anstatt Werte im Shader fest zu kodieren, könnten wir auch QML-Eigenschaften verwenden, die als Uniformen dargestellt werden, sogar animierte:

Änderung in main.qml, material.fragErgebnis
materials: CustomMaterial {
    fragmentShader: "material.frag"
    property color baseColor: "black"
    ColorAnimation on baseColor {
        from: "black"; to: "purple"; duration: 5000; loops: -1
    }
}
void MAIN()
{
    BASE_COLOR = vec4(baseColor.rgb, 1.0);
    EMISSIVE_COLOR = vec3(0.4);
}

Lassen Sie uns etwas weniger Triviales tun, etwas, das nicht mit PrincipledMaterial und seinen standardmäßigen, eingebauten Eigenschaften implementiert werden kann. Das folgende Material visualisiert die UV-Koordinaten der Textur des Würfelmeshes. U geht von 0 bis 1, also von Schwarz nach Rot, während V ebenfalls von 0 bis 1 geht, also von Schwarz nach Grün.

Änderung in main.qml, material.fragErgebnis
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(UV0, 0.0, 1.0);
}

Wenn wir schon dabei sind, warum nicht auch die Normalen visualisieren, dieses Mal auf einer Kugel. Wenn ein benutzerdefiniertes Vertex-Shader-Snippet den Wert von NORMAL ändert, würde der interpolierte Wert pro Fragment im Fragment-Shader, der ebenfalls den Namen NORMAL trägt, diese Anpassungen widerspiegeln, genau wie bei UVs.

Änderung in main.qml, material.fragErgebnis
Model {
    source: "#Sphere"
    scale: Qt.vector3d(2, 2, 2)
    materials: CustomMaterial {
        fragmentShader: "material.frag"
    }
}
void MAIN()
{
    BASE_COLOR = vec4(NORMAL, 1.0);
}

Farben

Wechseln wir für einen Moment zu einem Teekannenmodell, machen wir das Material zu einer Mischung aus Metall und Dielektrikum und versuchen wir, eine grüne Grundfarbe dafür festzulegen. Der Wert green QColor wird auf (0, 128, 0) abgebildet, worauf unser erster Versuch basieren könnte:

main.qml, material.frag
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color; clearColor: "black" }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "teapot.mesh"
            scale: Qt.vector3d(60, 60, 60)
            eulerRotation.x: 30
            materials: CustomMaterial {
                fragmentShader: "material.frag"
            }
        }
    }
}
void MAIN()
{
    BASE_COLOR = vec4(0.0, 0.5, 0.0, 1.0);
    METALNESS = 0.6;
    SPECULAR_AMOUNT = 0.4;
    ROUGHNESS = 0.4;
}

Das sieht nicht ganz richtig aus. Vergleichen Sie mit dem zweiten Ansatz:

Änderung in main.qml, material.fragErgebnis
materials: CustomMaterial {
    fragmentShader: "material.frag"
    property color uColor: "green"
}
void MAIN()
{
    BASE_COLOR = vec4(uColor.rgb, 1.0);
    METALNESS = 0.6;
    SPECULAR_AMOUNT = 0.4;
    ROUGHNESS = 0.4;
}

Wenn wir zu PrincipledMaterial wechseln, können wir bestätigen, dass das Ergebnis identisch mit unserem zweiten Ansatz ist, wenn wir PrincipledMaterial::baseColor auf "grün" setzen und die Metallizität und andere Eigenschaften beachten:

Änderung in main.qmlErgebnis
materials: PrincipledMaterial {
    baseColor: "green"
    metalness: 0.6
    specularAmount: 0.4
    roughness: 0.4
}

Wenn der Typ der Eigenschaft uColor auf vector4d oder einen anderen Typ als color geändert würde, würden sich die Ergebnisse plötzlich ändern und mit unserem ersten Ansatz identisch werden.

Warum ist das so?

Die Antwort liegt in der Konvertierung von sRGB in linear, die implizit für Farbeigenschaften von DefaultMaterial, PrincipledMaterial, und auch für benutzerdefinierte Eigenschaften mit einem color Typ in einem CustomMaterial durchgeführt wird. Eine solche Konvertierung wird für keinen anderen Wert durchgeführt, wenn also der Shader einen Farbwert fest kodiert oder auf einer QML-Eigenschaft mit einem anderen Typ als color basiert, liegt es am Shader, die Linearisierung durchzuführen, falls der Quellwert im sRGB-Farbraum war. Die Konvertierung in den linearen Farbraum ist wichtig, da Qt Quick 3D tonemapping die Ergebnisse des Fragment-Shadings verarbeitet und dieser Prozess Werte im sRGB-Farbraum als Eingabe voraussetzt.

Die eingebauten QColor Konstanten, wie z.B. "green", sind alle im sRGB-Raum angegeben. Daher ist die Zuweisung von vec4(0.0, 0.5, 0.0, 1.0) an BASE_COLOR im ersten Versuch unzureichend, wenn wir ein Ergebnis wünschen, das einem RGB-Wert (0, 128, 0) im sRGB-Raum entspricht. In der Dokumentation BASE_COLOR unter CustomMaterial finden Sie eine Formel zur Linearisierung solcher Farbwerte. Dasselbe gilt für Farbwerte, die durch Sampling von Texturen gewonnen werden: Wenn die Quellbilddaten nicht im sRGB-Farbraum liegen, ist eine Konvertierung erforderlich (es sei denn, tonemapping ist deaktiviert).

Überblenden

Es reicht nicht aus, einfach einen Wert kleiner als 1.0 in BASE_COLOR.a zu schreiben, wenn man Alpha-Blending erreichen will. Solche Materialien werden sehr oft die Werte der Eigenschaften sourceBlend und destinationBlend ändern, um die gewünschten Ergebnisse zu erzielen.

Denken Sie auch daran, dass der kombinierte Alphawert der Node opacity multipliziert mit dem Materialalphawert ist.

Zur Veranschaulichung verwenden wir einen Shader, der BASE_COLOR die Farbe Rot mit Alpha 0.5 zuweist:

main.qml, material.fragErgebnis
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "white"
        }
        PerspectiveCamera {
            id: camera
            z: 600
        }
        DirectionalLight { }
        Model {
            source: "#Cube"
            x: -150
            eulerRotation.x: 60
            eulerRotation.y: 20
            materials: CustomMaterial {
                fragmentShader: "material.frag"
            }
        }
        Model {
            source: "#Cube"
            eulerRotation.x: 60
            eulerRotation.y: 20
            materials: CustomMaterial {
                sourceBlend: CustomMaterial.SrcAlpha
                destinationBlend: CustomMaterial.OneMinusSrcAlpha
                fragmentShader: "material.frag"
            }
        }
        Model {
            source: "#Cube"
            x: 150
            eulerRotation.x: 60
            eulerRotation.y: 20
            materials: CustomMaterial {
                sourceBlend: CustomMaterial.SrcAlpha
                destinationBlend: CustomMaterial.OneMinusSrcAlpha
                fragmentShader: "material.frag"
            }
            opacity: 0.5
        }
    }
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 0.5);
}

Der erste Würfel schreibt 0,5 in den Alphawert der Farbe, aber er bringt keine sichtbaren Ergebnisse, da Alpha Blending nicht aktiviert ist. Der zweite Würfel aktiviert einfaches Alpha Blending über die CustomMaterial Eigenschaften. Der dritte Würfel weist dem Modell ebenfalls eine Deckkraft von 0,5 zu, was bedeutet, dass die effektive Deckkraft 0,25 beträgt.

Weitergabe von Daten zwischen dem Vertex- und Fragment-Shader

Berechnung eines Wertes pro Scheitelpunkt (z. B. bei einem einzelnen Dreieck für die drei Ecken des Dreiecks) und anschließende Weitergabe an die Fragmentstufe, wo für jedes Fragment (z. B. jedes vom gerasterten Dreieck abgedeckte Fragment) ein interpolierter Wert zugänglich gemacht wird. In benutzerdefinierten Material-Shader-Snippets wird dies durch das Schlüsselwort VARYING ermöglicht. Dies bietet eine ähnliche Syntax wie GLSL 120 und GLSL ES 100, funktioniert aber unabhängig von der zur Laufzeit verwendeten Grafik-API. Die Engine kümmert sich darum, die unterschiedlichen Deklarationen entsprechend umzuschreiben.

Schauen wir uns an, wie das klassische Textursampling mit UV-Koordinaten aussehen würde. Texturen werden in einem späteren Abschnitt behandelt, jetzt wollen wir uns darauf konzentrieren, wie wir die UV-Koordinaten erhalten, die an die Funktion texture() im Shader übergeben werden können.

main.qml, material.vert, material.frag
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color; clearColor: "black" }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Sphere"
            scale: Qt.vector3d(4, 4, 4)
            eulerRotation.x: 30
            materials: CustomMaterial {
                vertexShader: "material.vert"
                fragmentShader: "material.frag"
                property TextureInput someTextureMap: TextureInput {
                    texture: Texture {
                        source: "qt_logo_rect.png"
                    }
                }
            }
        }
    }
}
VARYING vec2 uv;
void MAIN()
{
    uv = UV0;
}
VARYING vec2 uv;
void MAIN()
{
    BASE_COLOR = texture(someTextureMap, uv);
}
qt_logo_rect.pngErgebnis

Beachten Sie, dass VARYING Deklarationen. Der Name und der Typ müssen übereinstimmen, uv im Fragment-Shader wird die interpolierte UV-Koordinate für das aktuelle Fragment ausgeben.

Jede andere Art von Daten kann auf ähnliche Weise an die Fragmentstufe weitergegeben werden. Es ist erwähnenswert, dass es in vielen Fällen nicht notwendig ist, eigene Materialvariationen einzurichten, da es Built-Ins gibt, die viele der typischen Anforderungen abdecken. Dazu gehört die Erstellung der (interpolierten) Normalen, UVs, der Weltposition (VAR_WORLD_POSITION) oder des Vektors, der auf die Kamera zeigt (VIEW_VECTOR).

Das obige Beispiel kann in der Tat zu folgendem vereinfacht werden, da UV0 auch in der Fragmentphase automatisch verfügbar ist:

Änderung in main.qml, material.fragErgebnis
materials: CustomMaterial {
    fragmentShader: "material.frag"
    property TextureInput someTextureMap: TextureInput {
        texture: Texture {
        source: "qt_logo_rect.png"
    }
}
void MAIN()
{
    BASE_COLOR = texture(someTextureMap, UV0);
}

Um die Interpolation für eine Variable zu deaktivieren, verwenden Sie das Schlüsselwort flat sowohl im Vertex- als auch im Fragment-Shader-Snippet. Zum Beispiel:

VARYING flat vec2 v;

Texturen

Ein CustomMaterial hat keine eingebauten Textur-Maps, d.h. es gibt kein Äquivalent zu z.B. PrincipledMaterial::baseColorMap. Das liegt daran, dass die Implementierung derselben oft trivial ist, während sie viel mehr Flexibilität bietet als das, was DefaultMaterial und PrincipledMaterial eingebaut haben. Neben dem einfachen Abtasten einer Textur steht es benutzerdefinierten Fragment-Shader-Snippets frei, Daten aus verschiedenen Quellen zu kombinieren und zu mischen, wenn sie die Werte berechnen, die sie BASE_COLOR, EMISSIVE_COLOR, ROUGHNESS usw. zuweisen. Sie können diese Berechnungen auf Daten stützen, die über QML-Eigenschaften bereitgestellt werden, auf interpolierte Daten, die von der Vertex-Stufe gesendet werden, auf Werte, die von gesampelten Texturen abgerufen werden, und auf fest kodierte Werte.

Wie das vorangegangene Beispiel zeigt, ist die Freigabe einer Textur für den Vertex-, Fragment- oder beide Shader sehr ähnlich wie bei skalaren und vektoriellen Einheitswerten: eine QML-Eigenschaft vom Typ TextureInput wird im Shader-Code automatisch mit sampler2D verknüpft. Wie immer ist es nicht notwendig, diesen Sampler im Shader-Code zu deklarieren.

Ein TextureInput referenziert ein Texture, mit einer zusätzlichen enabled Eigenschaft. Ein Texture kann seine Daten auf drei Arten beziehen: from an image file, from a texture with live Qt Quick content, oder can be provided from C++ über QQuick3DTextureData.

Hinweis: Wenn es um die Texture Eigenschaften geht, sind die Quelle, die Kacheln und die Filterung die einzigen, die bei benutzerdefinierten Materialien implizit berücksichtigt werden, da der Rest (z. B. UV-Transformationen) von den benutzerdefinierten Shadern nach eigenem Ermessen implementiert werden muss.

Sehen wir uns ein Beispiel an, bei dem ein Modell, in diesem Fall eine Kugel, mit Live-Inhalten von Qt Quick texturiert wird:

main.qml, material.frag
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color; clearColor: "black" }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Sphere"
            scale: Qt.vector3d(4, 4, 4)
            eulerRotation.x: 30
            materials: CustomMaterial {
                fragmentShader: "material.frag"
                property TextureInput someTextureMap: TextureInput {
                    texture: Texture {
                        sourceItem: Rectangle {
                            width: 512; height: 512
                            color: "red"
                            Rectangle {
                                width: 32; height: 32
                                anchors.horizontalCenter: parent.horizontalCenter
                                y: 150
                                color: "gray";
                                NumberAnimation on rotation { from: 0; to: 360; duration: 3000; loops: -1 }
                            }
                            Text {
                                anchors.centerIn: parent
                                text: "Texture Map"
                                font.pointSize: 16
                            }
                        }
                    }
                }
            }
        }
    }
}
void MAIN()
{
    vec2 uv = vec2(UV0.x, 1.0 - UV0.y);
    vec4 c = texture(someTextureMap, uv);
    BASE_COLOR = c;
}

Hier wird der 2D-Teilbaum (Rechteck mit zwei Kindern: ein weiteres Rechteck und der Text) jedes Mal, wenn sich diese Mini-Szene ändert, in eine 512x512 2D-Textur gerendert. Die Textur wird dann dem benutzerdefinierten Material mit dem Namen someTextureMap zugewiesen.

Beachten Sie die Umkehrung der V-Koordinate im Shader. Wie bereits erwähnt, bieten benutzerdefinierte Materialien, bei denen die volle Programmierbarkeit auf Shader-Ebene gegeben ist, nicht die "festen" Funktionen von Texture und PrincipledMaterial. Das bedeutet, dass alle Transformationen der UV-Koordinaten durch den Shader vorgenommen werden müssen. Hier wissen wir, dass die Textur über Texture::sourceItem generiert wird und dass V gespiegelt werden muss, um etwas zu erhalten, das mit dem UV-Satz des von uns verwendeten Meshes übereinstimmt.

Was dieses Beispiel zeigt, kann man auch mit PrincipledMaterial machen. Machen wir es noch interessanter, indem wir zusätzlich einen einfachen Prägeeffekt erzeugen:

material.fragErgebnis
void MAIN()
{
    vec2 uv = vec2(UV0.x, 1.0 - UV0.y);
    vec2 size = vec2(textureSize(someTextureMap, 0));
    vec2 d = vec2(1.0 / size.x, 1.0 / size.y);
    vec4 diff = texture(someTextureMap, uv + d) - texture(someTextureMap, uv - d);
    float c = (diff.x + diff.y + diff.z) + 0.5;
    BASE_COLOR = vec4(c, c, c, 1.0);
}

Mit den bisher behandelten Funktionen steht eine breite Palette von Möglichkeiten offen, um Materialien zu erstellen, die die Meshes auf visuell beeindruckende Weise schattieren. Zum Abschluss der grundlegenden Tour sehen wir uns ein Beispiel an, das Höhen- und Normal-Maps auf ein ebenes Mesh anwendet. (Hier wird eine eigene .mesh Datei verwendet, da die eingebaute #Rectangle nicht über genügend Unterteilungen verfügt) Für bessere Beleuchtungsergebnisse werden wir eine bildbasierte Beleuchtung mit einem 360-Grad-HDR-Bild verwenden. Das Bild wird auch als Skybox festgelegt, um das Geschehen zu verdeutlichen.

Beginnen wir mit einer leeren CustomMaterial:

main.qmlErgebnis
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.SkyBox
            lightProbe: Texture {
                source: "00489_OpenfootageNET_snowfield_low.hdr"
            }
        }
        PerspectiveCamera {
            z: 600
        }
        Model {
            source: "plane.mesh"
            scale: Qt.vector3d(400, 400, 400)
            z: 400
            y: -50
            eulerRotation.x: -90
            materials: CustomMaterial { }
        }
    }
}

Nun wollen wir einige Shader erstellen, die eine Height- und Normal-Map auf das Mesh anwenden:

HöhenkarteNormap-Map

material.vert, material.frag
float getHeight(vec2 pos)
{
    return texture(heightMap, pos).r;
}

void MAIN()
{
    const float offset = 0.004;
    VERTEX.y += getHeight(UV0);
    TANGENT = normalize(vec3(0.0, getHeight(UV0 + vec2(0.0, offset)) - getHeight(UV0 + vec2(0.0, -offset)), offset * 2.0));
    BINORMAL = normalize(vec3(offset * 2.0, getHeight(UV0 + vec2(offset, 0.0)) - getHeight(UV0 + vec2(-offset, 0.0)), 0.0));
    NORMAL = cross(TANGENT, BINORMAL);
}
void MAIN()
{
    vec3 normalValue = texture(normalMap, UV0).rgb;
    normalValue.xy = normalValue.xy * 2.0 - 1.0;
    normalValue.z = sqrt(max(0.0, 1.0 - dot(normalValue.xy, normalValue.xy)));
    NORMAL = normalize(mix(NORMAL, TANGENT * normalValue.x + BINORMAL * normalValue.y + NORMAL * normalValue.z, 1.0));
}
Änderung in main.qmlErgebnis
materials: CustomMaterial {
    vertexShader: "material.vert"
    fragmentShader: "material.frag"
    property TextureInput normalMap: TextureInput {
        texture: Texture { source: "normalmap.jpg" }
    }
    property TextureInput heightMap: TextureInput {
        texture: Texture { source: "heightmap.png" }
    }
}

Hinweis: Das WasdController Objekt kann bei der Entwicklung und Fehlersuche sehr hilfreich sein, da es das Navigieren und Umschauen in der Szene mit Tastatur und Maus in gewohnter Weise ermöglicht. Eine Kamera, die von WasdController gesteuert wird, ist so einfach wie:

import QtQuick3D.Helpers
View3D {
    PerspectiveCamera {
        id: camera
    }
    // ...
}
WasdController {
    controlledObject: camera
}

Tiefe und Bildschirmtexturen

Wenn ein benutzerdefiniertes Shader-Snippet die Schlüsselwörter DEPTH_TEXTURE oder SCREEN_TEXTURE verwendet, werden die entsprechenden Texturen in einem separaten Rendering-Durchgang erzeugt, was nicht unbedingt eine billige Operation ist, aber die Implementierung einer Vielzahl von Techniken ermöglicht, wie z. B. Brechung für glasartige Materialien.

DEPTH_TEXTURE ist eine sampler2D, die es ermöglicht, eine Textur mit dem Inhalt des Tiefenpuffers zu sampeln, wobei alle opaque Objekte in der Szene gerendert werden. In ähnlicher Weise ist SCREEN_TEXTURE eine sampler2D, die es ermöglicht, eine Textur mit dem Inhalt der Szene zu sampeln, wobei alle transparenten Materialien oder alle Materialien, die ebenfalls die SCREEN_TEXTURE verwenden, ausgeschlossen werden. Die Textur kann für Materialien verwendet werden, die den Inhalt des Framebuffers benötigen, in den sie gerendert werden. Die SCREEN_TEXTURE-Textur verwendet denselben Clear-Modus wie die View3D. Die Größe dieser Texturen entspricht der Größe der View3D in Pixeln.

Lassen Sie uns eine einfache Demonstration machen, indem wir den Inhalt des Tiefenpuffers über DEPTH_TEXTURE visualisieren. Die far clip value der Kamera wird hier von der Standardeinstellung 10000 auf 2000 reduziert, um einen kleineren Bereich zu haben und so die visualisierten Tiefenwertunterschiede deutlicher zu machen. Das Ergebnis ist ein Rechteck, das zufällig den Tiefenpuffer für die Szene über seine Oberfläche visualisiert.

main.qml, material.fragErgebnis
import QtQuick
import QtQuick3D
import QtQuick3D.Helpers
Rectangle {
    width: 400
    height: 400
    color: "black"
    View3D {
        anchors.fill: parent
        PerspectiveCamera {
            id: camera
            z: 600
            clipNear: 1
            clipFar: 2000
        }
        DirectionalLight { }
        Model {
            source: "#Cube"
            scale: Qt.vector3d(2, 2, 2)
            position: Qt.vector3d(150, 200, -1000)
            eulerRotation.x: 60
            eulerRotation.y: 20
            materials: PrincipledMaterial { }
        }
        Model {
            source: "#Cylinder"
            scale: Qt.vector3d(2, 2, 2)
            position: Qt.vector3d(400, 200, -1000)
            materials: PrincipledMaterial { }
            opacity: 0.3
        }
        Model {
            source: "#Sphere"
            scale: Qt.vector3d(2, 2, 2)
            position: Qt.vector3d(-150, 200, -600)
            materials: PrincipledMaterial { }
        }
        Model {
            source: "#Cone"
            scale: Qt.vector3d(2, 2, 2)
            position: Qt.vector3d(0, 400, -1200)
            materials: PrincipledMaterial { }
        }
        Model {
            source: "#Rectangle"
            scale: Qt.vector3d(3, 3, 3)
            y: -150
            materials: CustomMaterial {
                fragmentShader: "material.frag"
            }
        }
    }
    WasdController {
        controlledObject: camera
    }
}
void MAIN()
{
    float zNear = CAMERA_PROPERTIES.x;
    float zFar = CAMERA_PROPERTIES.y;
    float zRange = zFar - zNear;
    vec4 depthSample = texture(DEPTH_TEXTURE, vec2(UV0.x, 1.0 - UV0.y));
    float zn = 2.0 * depthSample.r - 1.0;
    float d = 2.0 * zNear * zFar / (zFar + zNear - zn * zRange);
    d /= zFar;
    BASE_COLOR = vec4(d, d, d, 1.0);
}

Beachten Sie, dass der Zylinder in DEPTH_TEXTURE nicht vorhanden ist, da er auf Semitransparenz angewiesen ist, was ihn in eine andere Kategorie als die anderen Objekte stellt, die alle undurchsichtig sind. Diese Objekte schreiben nicht in den Tiefenpuffer, obwohl sie gegen die von opaken Objekten geschriebenen Tiefenwerte testen, und sind darauf angewiesen, dass sie in der Reihenfolge von hinten nach vorne gerendert werden. Daher sind sie auch in DEPTH_TEXTURE nicht vorhanden.

Was passiert, wenn wir den Shader stattdessen auf das Sample SCREEN_TEXTURE umstellen?

material.fragErgebnis
void MAIN()
{
    vec4 c = texture(SCREEN_TEXTURE, vec2(UV0.x, 1.0 - UV0.y));
    if (c.a == 0.0)
        c.rgb = vec3(0.2, 0.1, 0.3);
    BASE_COLOR = c;
}

Hier wird das Rechteck mit SCREEN_TEXTURE texturiert, wobei die transparenten Pixel durch lila ersetzt werden.

Lichtprozessorfunktionen

Ein fortschrittliches Merkmal von CustomMaterial ist die Möglichkeit, Funktionen im Fragment-Shader zu definieren, die die Beleuchtungsgleichungen, die zur Berechnung der Fragmentfarbe verwendet werden, neu implementieren. Eine Lichtprozessorfunktion, sofern vorhanden, wird für jedes Licht in der Szene und für jedes Fragment einmal aufgerufen. Es gibt eine eigene Funktion für die verschiedenen Lichttypen sowie für den Umgebungslicht- und Spiegellichtanteil. Wenn keine entsprechende Lichtprozessorfunktion vorhanden ist, werden die Standardberechnungen verwendet, so wie es auch PrincipledMaterial tun würde. Wenn ein Lichtprozessor vorhanden ist, aber der Funktionskörper leer ist, bedeutet dies, dass es keinen Beitrag eines bestimmten Lichttyps in der Szene gibt.

Einzelheiten zu Funktionen wie DIRECTIONAL_LIGHT, POINT_LIGHT, SPOT_LIGHT, AMBIENT_LIGHT und SPECULAR_LIGHT finden Sie in der Dokumentation CustomMaterial.

Nicht schattierte benutzerdefinierte Materialien

Es gibt einen weiteren Typ von CustomMaterial: unshaded benutzerdefinierte Materialien. In allen bisherigen Beispielen wurden shaded benutzerdefinierte Materialien verwendet, wobei die Eigenschaft shadingMode auf ihrem Standardwert CustomMaterial.schattiert belassen wurde.

Was passiert, wenn wir diese Eigenschaft auf CustomMaterial.Unshaded setzen?

Zunächst einmal haben Schlüsselwörter wie BASE_COLOR, EMISSIVE_COLOR, METALNESS, usw. nicht mehr den gewünschten Effekt. Das liegt daran, dass ein nicht schattiertes Material, wie der Name schon sagt, nicht automatisch mit einem Großteil des Standard-Schattierungscodes ergänzt wird und somit Lichter, bildbasierte Beleuchtung, Schatten und Umgebungsokklusion in der Szene ignoriert. Vielmehr gibt ein nicht schattiertes Material dem Shader über das Schlüsselwort FRAGCOLOR die volle Kontrolle. Dies ist vergleichbar mit gl_FragColor: Die Farbe, die FRAGCOLOR zugewiesen wird, ist das Ergebnis und die endgültige Farbe des Fragments, ohne weitere Anpassungen durch Qt Quick 3D.

main.qml, material.frag, material2.fragErgebnis
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }
        PerspectiveCamera { z: 600 }
        DirectionalLight { }
        Model {
            source: "#Cylinder"
            x: -100
            eulerRotation.x: 30
            materials: CustomMaterial {
                fragmentShader: "material.frag"
            }
        }
        Model {
            source: "#Cylinder"
            x: 100
            eulerRotation.x: 30
            materials: CustomMaterial {
                shadingMode: CustomMaterial.Unshaded
                fragmentShader: "material2.frag"
            }
        }
    }
}
void MAIN()
{
    BASE_COLOR = vec4(1.0);
}
void MAIN()
{
    FRAGCOLOR = vec4(1.0);
}

Beachten Sie, dass der rechte Zylinder die DirectionalLight in der Szene ignoriert. Seine Schattierung weiß nichts über die Beleuchtung der Szene, die endgültige Fragmentfarbe ist ganz weiß.

Der Vertex-Shader in einem nicht schattierten Material hat immer noch die typischen Eingaben zur Verfügung: VERTEX, NORMAL, MODELVIEWPROJECTION_MATRIX, usw. und kann auf POSITION schreiben. Der Fragment-Shader verfügt jedoch nicht mehr über die gleichen Annehmlichkeiten: NORMAL, UV0 oder VAR_WORLD_POSITION sind im Fragment-Shader eines nicht schattierten Materials nicht verfügbar. Stattdessen muss der Shader-Code nun alles, was er zur Bestimmung der endgültigen Fragmentfarbe benötigt, selbst berechnen und mit VARYING weitergeben.

Schauen wir uns ein Beispiel an, das sowohl einen Vertex- als auch einen Fragment-Shader hat. Die veränderte Scheitelpunktposition wird an den Fragment-Shader weitergegeben, wobei jedem Fragment ein interpolierter Wert zur Verfügung gestellt wird.

main.qml, material.vert, material.frag
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }
        PerspectiveCamera { z: 600 }
        Model {
            source: "#Sphere"
            scale: Qt.vector3d(3, 3, 3)
            materials: CustomMaterial {
                property real time: 0.0
                NumberAnimation on time { from: 0; to: 100; duration: 20000; loops: -1 }
                property real amplitude: 10.0
                shadingMode: CustomMaterial.Unshaded
                vertexShader: "material.vert"
                fragmentShader: "material.frag"
            }
        }
    }
}
VARYING vec3 pos;
void MAIN()
{
    pos = VERTEX;
    pos.x += sin(time * 4.0 + pos.y) * amplitude;
    POSITION = MODELVIEWPROJECTION_MATRIX * vec4(pos, 1.0);
}
VARYING vec3 pos;
void MAIN()
{
    FRAGCOLOR = vec4(vec3(pos.x * 0.02, pos.y * 0.02, pos.z * 0.02), 1.0);
}

Ungeschattete Materialien sind nützlich, wenn eine Interaktion mit der Szenenbeleuchtung nicht notwendig oder erwünscht ist und das Material die volle Kontrolle über die endgültige Fragmentfarbe haben muss. Beachten Sie, dass das obige Beispiel weder eine DirectionalLight noch andere Lichter hat, aber die Kugel mit dem benutzerdefinierten Material wird wie erwartet angezeigt.

Hinweis: Ein nicht schattiertes Material, das nur über ein Vertex-Shader-Snippet verfügt, aber die Eigenschaft fragmentShader nicht angibt, ist zwar immer noch funktionsfähig, aber die Ergebnisse sind so, als ob der ShadingMode auf Shaded gesetzt wäre. Daher ist es wenig sinnvoll, den ShadingMode für Materialien zu ändern, die nur einen Vertex-Shader haben.

Programmierbarkeit für Effekte

Nachbearbeitungseffekte wenden einen oder mehrere Fragment-Shader auf das Ergebnis eines View3D an. Die Ausgabe dieser Fragment-Shader wird dann anstelle der ursprünglichen Rendering-Ergebnisse angezeigt. Dies ist konzeptionell sehr ähnlich zu Qt Quick's ShaderEffect und ShaderEffectSource.

Hinweis: Nachbearbeitungseffekte sind nur verfügbar, wenn renderMode für View3D auf View3D eingestellt ist.

Es können auch benutzerdefinierte Vertex-Shader-Snippets für einen Effekt angegeben werden, aber sie sind nur begrenzt nützlich und werden daher voraussichtlich relativ selten verwendet. Die Scheitelpunkt-Eingabe für einen Post-Processing-Effekt ist ein Quad (entweder zwei Dreiecke oder ein Dreiecksstreifen), das Transformieren oder Verschieben der Scheitelpunkte davon ist oft nicht hilfreich. Es kann jedoch sinnvoll sein, einen Vertex-Shader zu haben, um Daten zu berechnen und mit dem Schlüsselwort VARYING an den Fragment-Shader weiterzugeben. Wie üblich erhält der Fragment-Shader dann einen interpolierten Wert, der auf der aktuellen Fragmentkoordinate basiert.

Die Syntax der Shader-Snippets, die mit einem Effect verbunden sind, ist identisch mit den Shadern für ein nicht schattiertes CustomMaterial. Was die eingebauten speziellen Schlüsselwörter betrifft, so funktionieren VARYING, MAIN, FRAGCOLOR (nur Fragment-Shader), POSITION (nur Vertex-Shader), VERTEX (nur Vertex-Shader) und MODELVIEWPROJECTION_MATRIX identisch mit CustomMaterial.

Die wichtigsten speziellen Schlüsselwörter für Effect Fragment-Shader sind die folgenden:

NameTypBeschreibung
INPUTsampler2D oder sampler2DArrayDer Sampler für die Eingabetextur. Ein Effekt wird diese typischerweise mit INPUT_UV sampeln.
EINGABE_UVvec2UV-Koordinaten für das Sampling INPUT.
INPUT_SIZEvec2Die Größe der INPUT Textur, in Pixeln. Dies ist eine praktische Alternative zum Aufruf von textureSize().
AUSGABE_GRÖSSEvec2Die Größe der Ausgabetextur, in Pixeln. In vielen Fällen gleich INPUT_SIZE, aber ein Multi-Pass-Effekt kann Durchgänge haben, die zu Zwischentexturen mit unterschiedlichen Größen führen.
TIEFEN_TEXTURsampler2DTiefentextur mit dem Inhalt des Tiefenpuffers mit den undurchsichtigen Objekten in der Szene. Wie bei CustomMaterial löst das Vorhandensein dieses Schlüsselworts im Shader die automatische Erzeugung der Tiefentextur aus.

Hinweis: Wenn Multiview-Rendering aktiviert ist, ist die Eingabetextur ein 2D-Textur-Array. GLSL-Funktionen wie texture() und textureSize() nehmen/geben dann einen vec3 bzw.ivec3 zurück. Verwenden Sie VIEW_INDEX für die Ebene. In VR/AR-Anwendungen, die sowohl mit als auch ohne Multiview-Rendering funktionieren sollen, besteht der portable Ansatz darin, den Shader-Code so zu schreiben:

#if QSHADER_VIEW_COUNT >= 2
    vec4 c = texture(INPUT, vec3(INPUT_UV, VIEW_INDEX));
#else
    vec4 c = texture(INPUT, INPUT_UV);
#endif

Ein Post-Processing-Effekt

Beginnen wir mit einer einfachen Szene, dieses Mal mit ein paar mehr Objekten, einschließlich eines texturierten Rechtecks, das eine Schachbrett-Textur als Basis-Farbkarte verwendet.

main.qmlErgebnis
import QtQuick
import QtQuick3D
Item {
    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "black"
        }

        PerspectiveCamera { z: 400 }

        DirectionalLight { }

        Texture {
            id: checkerboard
            source: "checkerboard.png"
            scaleU: 20
            scaleV: 20
            tilingModeHorizontal: Texture.Repeat
            tilingModeVertical: Texture.Repeat
        }

        Model {
            source: "#Rectangle"
            scale: Qt.vector3d(10, 10, 1)
            eulerRotation.x: -45
            materials: PrincipledMaterial {
                baseColorMap: checkerboard
            }
        }

        Model {
            source: "#Cone"
            position: Qt.vector3d(100, -50, 100)
            materials: PrincipledMaterial { }
        }

        Model {
            source: "#Cube"
            position.y: 100
            eulerRotation.y: 20
            materials: PrincipledMaterial { }
        }

        Model {
            source: "#Sphere"
            position: Qt.vector3d(-150, 200, -100)
            materials: PrincipledMaterial { }
        }
    }
}

Nun wollen wir einen Effekt auf die gesamte Szene anwenden. Genauer gesagt, auf die View3D. Wenn es mehrere View3D in der Szene gibt, hat jedes seine eigene SceneEnvironment und somit auch seine eigene Nachbearbeitungs-Effektkette. In diesem Beispiel gibt es ein einziges View3D, das das gesamte Fenster abdeckt.

Änderung in main.qmleffect.frag
environment: SceneEnvironment {
    backgroundMode: SceneEnvironment.Color
    clearColor: "black"
    effects: redEffect
}

Effect {
    id: redEffect
    property real uRed: 1.0
    NumberAnimation on uRed { from: 1; to: 0; duration: 5000; loops: -1 }
    passes: Pass {
        shaders: Shader {
            stage: Shader.Fragment
            shader: "effect.frag"
        }
    }
}
void MAIN()
{
    vec4 c = texture(INPUT, INPUT_UV);
    c.r = uRed;
    FRAGCOLOR = c;
}

Dieser einfache Effekt ändert den Wert des roten Farbkanals. Die Offenlegung von QML-Eigenschaften als Uniformen funktioniert bei Effekten auf die gleiche Weise wie bei benutzerdefinierten Materialien. Der Shader beginnt mit einer Zeile, die beim Schreiben von Fragment-Shadern für Effekte sehr häufig vorkommt: Abtasten von INPUT an den UV-Koordinaten INPUT_UV. Dann führt er die gewünschten Berechnungen durch und weist die endgültige Fragmentfarbe FRAGCOLOR zu.

Viele Eigenschaften, die in diesem Beispiel festgelegt werden, sind im Plural (Effekte, Pässe, Shader). Während die Syntax der Liste [ ] weggelassen werden kann, wenn nur ein einziges Element vorhanden ist, sind alle diese Eigenschaften Listen und können mehr als ein Element enthalten. Warum ist das so?

  • effects ist eine Liste, weil View3D es erlaubt, mehrere Effekte miteinander zu verketten. Die Effekte werden in der Reihenfolge angewendet, in der sie der Liste hinzugefügt werden. Dies ermöglicht die einfache Anwendung von zwei oder mehr Effekten auf View3D und ähnelt dem, was man in Qt Quick durch Verschachtelung von ShaderEffect Elementen erreichen kann. Die INPUT Textur des nächsten Effekts ist immer eine Textur, die die Ausgabe des vorherigen Effekts enthält. Die Ausgabe des letzten Effekts, die als endgültige Ausgabe des View3D verwendet wird, ist eine Liste.
  • passes ist eine Liste, denn im Gegensatz zu ShaderEffect hat Effect eine eingebaute Unterstützung für mehrere Durchläufe. Ein Effekt mit mehreren Durchläufen ist leistungsfähiger als die Verkettung mehrerer, unabhängiger Effekte in effects: Ein Durchlauf kann eine temporäre Zwischentextur ausgeben, die dann als Eingabe für nachfolgende Durchläufe verwendet werden kann, zusätzlich zur ursprünglichen Eingabetextur des Effekts. Auf diese Weise lassen sich komplexe Effekte erstellen, die mehrere Texturen berechnen, rendern und zusammenmischen, um die endgültige Fragmentfarbe zu erhalten. Dieser erweiterte Anwendungsfall wird hier nicht behandelt. Einzelheiten finden Sie auf der Dokumentationsseite Effect.
  • shaders ist eine Liste, da ein Effekt sowohl mit einem Vertex- als auch mit einem Fragment-Shader verbunden sein kann.

Verkettung mehrerer Effekte

Schauen wir uns ein Beispiel an, bei dem der Effekt aus dem vorherigen Beispiel durch einen anderen Effekt ergänzt wird, der dem eingebauten DistortionSpiral Effekt ähnelt.

Änderung in main.qmleffect2.frag
environment: SceneEnvironment {
    backgroundMode: SceneEnvironment.Color
    clearColor: "black"
    effects: [redEffect, distortEffect]
}

Effect {
    id: redEffect
    property real uRed: 1.0
    NumberAnimation on uRed { from: 1; to: 0; duration: 5000; loops: -1 }
    passes: Pass {
        shaders: Shader {
            stage: Shader.Fragment
            shader: "effect.frag"
        }
    }
}

Effect {
    id: distortEffect
    property real uRadius: 0.1
    NumberAnimation on uRadius { from: 0.1; to: 1.0; duration: 5000; loops: -1 }
    passes: Pass {
        shaders: Shader {
            stage: Shader.Fragment
            shader: "effect2.frag"
        }
    }
}
void MAIN()
{
    vec2 center_vec = INPUT_UV - vec2(0.5, 0.5);
    center_vec.y *= INPUT_SIZE.y / INPUT_SIZE.x;
    float dist_to_center = length(center_vec) / uRadius;
    vec2 texcoord = INPUT_UV;
    if (dist_to_center <= 1.0) {
        float rotation_amount = (1.0 - dist_to_center) * (1.0 - dist_to_center);
        float r = radians(360.0) * rotation_amount / 4.0;
        float cos_r = cos(r);
        float sin_r = sin(r);
        mat2 rotation = mat2(cos_r, sin_r, -sin_r, cos_r);
        texcoord = vec2(0.5, 0.5) + rotation * (INPUT_UV - vec2(0.5, 0.5));
    }
    vec4 c = texture(INPUT, texcoord);
    FRAGCOLOR = c;
}

Nun die vielleicht überraschende Frage: Warum ist dies ein schlechtes Beispiel?

Genauer gesagt ist es nicht schlecht, sondern zeigt vielmehr ein Muster, das zu vermeiden oft von Vorteil sein kann.

Die Verkettung von Effekten auf diese Weise kann nützlich sein, aber es ist wichtig, die Auswirkungen auf die Leistung im Auge zu behalten: Zwei Rendering-Durchläufe (einer zur Erzeugung einer Textur mit dem angepassten roten Farbkanal und ein weiterer zur Berechnung der Verzerrung) sind ziemlich verschwenderisch, wenn einer ausreichen würde. Wenn die Fragment-Shader-Schnipsel kombiniert würden, könnte das gleiche Ergebnis mit einem einzigen Effekt erzielt werden.

Definieren von Mesh- und Texturdaten in C++

Die prozedurale Erzeugung von Mesh- und Texturbilddaten erfolgt in ähnlichen Schritten:

  • Unterklasse QQuick3DGeometry oder QQuick3DTextureData
  • Setzen Sie die gewünschten Vertex- oder Bilddaten bei der Konstruktion, indem Sie die geschützten Mitgliedsfunktionen der Basisklasse aufrufen
  • Wenn später dynamische Änderungen erforderlich sind, setzen Sie die neuen Daten und rufen update() auf.
  • Sobald die Implementierung abgeschlossen ist, muss die Klasse registriert werden, um sie in QML sichtbar zu machen
  • Model und Texture Objekte in QML können nun den benutzerdefinierten Vertex- oder Bilddatenanbieter verwenden, indem sie die Model::geometry oder Texture::textureData Eigenschaft setzen

Benutzerdefinierte Vertex-Daten

Scheitelpunktdaten beziehen sich auf die Abfolge von Werten (typischerweise float), aus denen ein Netz besteht. Anstatt .mesh Dateien zu laden, ist ein benutzerdefinierter Geometrieanbieter für die Bereitstellung derselben Daten verantwortlich. Die Scheitelpunktdaten bestehen aus attributes, wie z. B. Position, Textur (UV)-Koordinaten oder Normalen. Die Spezifikation der Attribute beschreibt, welche Art von Attributen vorhanden ist, den Komponententyp (z. B. ein 3-Komponenten-Float-Vektor für die Scheitelpunktposition, bestehend aus x-, y- und z-Werten), an welchem Offset sie in den bereitgestellten Daten beginnen und wie groß der Stride (die Schrittweite, die zum Offset hinzugefügt werden muss, um auf das nächste Element für dasselbe Attribut zu zeigen) ist.

Dies mag vertraut erscheinen, wenn man mit Grafik-APIs wie OpenGL oder Vulkan direkt gearbeitet hat, denn die Art und Weise, wie die Vertex-Eingabe mit diesen APIs spezifiziert wird, entspricht in etwa dem, was eine .mesh -Datei oder eine QQuick3DGeometry -Instanz definiert.

Darüber hinaus muss auch die Mesh-Topologie (Primitivtyp) angegeben werden. Für indexiertes Zeichnen müssen auch die Daten für einen Indexpuffer angegeben werden.

Es gibt eine eingebaute benutzerdefinierte Geometrie-Implementierung: Das Modul QtQuick3D. Helpers enthält einen GridGeometry Typ. Dies ermöglicht das Rendern eines Gitters in der Szene mit Linienprimitiven, ohne eine eigene QQuick3DGeometry Unterklasse implementieren zu müssen.

Ein anderer häufiger Anwendungsfall ist das Rendern von Punkten. Dies ist recht einfach zu bewerkstelligen, da die Spezifikation der Attribute minimal sein wird: wir stellen drei Floats (x, y, z) für jeden Vertex zur Verfügung, sonst nichts. Eine QQuick3DGeometry Unterklasse könnte eine Geometrie, die aus 2000 Punkten besteht, ähnlich wie die folgende implementieren:

clear();
const int N = 2000;
const int stride = 3 * sizeof(float);
QByteArray v;
v.resize(N * stride);
float *p = reinterpret_cast<float *>(v.data());
QRandomGenerator *rg = QRandomGenerator::global();
for (int i = 0; i < N; ++i) {
    const float x = float(rg->bounded(200.0f) - 100.0f) / 20.0f;
    const float y = float(rg->bounded(200.0f) - 100.0f) / 20.0f;
    *p++ = x;
    *p++ = y;
    *p++ = 0.0f;
}
setVertexData(v);
setStride(stride);
setPrimitiveType(QQuick3DGeometry::PrimitiveType::Points);
addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0, QQuick3DGeometry::Attribute::F32Type);

Kombiniert mit einem Material von

DefaultMaterial {
    lighting: DefaultMaterial.NoLighting
    cullMode: DefaultMaterial.NoCulling
    diffuseColor: "yellow"
    pointSize: 4
}

sieht das Endergebnis etwa so aus (hier aus einem veränderten Kamerawinkel betrachtet, mit Hilfe von WasdController):

Hinweis: Beachten Sie, dass andere Punktgrößen und Linienbreiten als 1 zur Laufzeit möglicherweise nicht unterstützt werden, abhängig von der zugrunde liegenden Grafik-API. Dies ist etwas, worüber Qt keine Kontrolle hat. Daher kann es notwendig werden, alternative Techniken zu implementieren, anstatt sich auf das Zeichnen von Punkten und Linien zu verlassen.

Benutzerdefinierte Texturdaten

Bei Texturen sind die Daten, die bereitgestellt werden müssen, strukturell viel einfacher: Es sind die rohen Pixeldaten, mit einer unterschiedlichen Anzahl von Bytes pro Pixel, abhängig vom Texturformat. Eine RGBA -Textur erwartet beispielsweise vier Bytes pro Pixel, während RGBA16F vier Halbfloats pro Pixel vorsieht. Dies entspricht in etwa dem, was QImage intern speichert. Allerdings können Qt Quick 3D Texturen Formate haben, deren Daten nicht durch QImage dargestellt werden können. Zum Beispiel Fließkomma-HDR-Texturen oder komprimierte Texturen. Daher werden die Daten für QQuick3DTextureData immer als rohe Bytefolge bereitgestellt. Dies mag vertraut erscheinen, wenn man mit Grafik-APIs, wie OpenGL oder Vulkan direkt gearbeitet hat.

Einzelheiten finden Sie auf den Dokumentationsseiten QQuick3DGeometry und QQuick3DTextureData.

Siehe auch CustomMaterial, Effect, QQuick3DGeometry, QQuick3DTextureData, Qt Quick 3D - Beispiel für benutzerdefinierte Effekte, Qt Quick 3D - Beispiel für benutzerdefinierte Shader, Qt Quick 3D - Beispiel für benutzerdefinierte Materialien, Qt Quick 3D - Beispiel für benutzerdefinierte Geometrie, und Qt Quick 3D - Beispiel für prozedurale Texturen.

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.