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 PrincipledMaterial | Mit 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 Effekt | Die 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:
Funktion | Referenz Dokumentation | Relevante Beispiele |
---|---|---|
Benutzerdefinierte Materialien | CustomMaterial | Qt Quick 3D -Beispiel für benutzerdefinierte Shader, Qt Quick 3D - Beispiel für benutzerdefinierte Materialien |
Benutzerdefinierte Nachbearbeitungseffekte | Effect | Qt Quick 3D - Beispiel für benutzerdefinierte Effekte |
Benutzerdefinierte Geometrie | QQuick3DGeometry, Model::geometry | Qt Quick 3D - Beispiel für benutzerdefinierte Geometrie |
Benutzerdefinierte Texturdaten | QQuick3DTextureData, Texture::textureData | Qt Quick 3D - Beispiel für prozedurale Texturen |
Programmierbarkeit für Materialien
Nehmen wir eine Szene mit einem Würfel und beginnen wir mit einem Standard PrincipledMaterial und CustomMaterial:
PrincipledMaterial | CustomMaterial |
---|---|
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.vert | Ergebnis |
---|---|
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üsselwort | Typ | Beschreibung |
---|---|---|
MAIN | void MAIN() ist der Einstiegspunkt. Diese Funktion muss immer in einem benutzerdefinierten Vertex-Shader-Snippet vorhanden sein, andernfalls hat es keinen Sinn, eine Funktion bereitzustellen. | |
VERTEX | vec3 | Die 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. |
NORMAL | vec3 | Die 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. |
UV0 | vec2 | Der 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_MATRIX | Matte4 | Die 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_MATRIX | Matte4 | Die Modell-(Welt-)Matrix. Nur lesbar. |
NORMAL_MATRIX | mat3 | Die transponierte Umkehrung des linken oberen 3x3-Slice der Modellmatrix. Nur lesbar. |
KAMERA_LAGE | vec3 | Die Kameraposition im Weltraum. In den Beispielen auf dieser Seite ist dies (0, 0, 600) . Nur lesen. |
CAMERA_DIRECTION | vec3 | Der Richtungsvektor der Kamera. In den Beispielen auf dieser Seite ist dies (0, 0, -1) . Nur lesen. |
CAMERA_PROPERTIES | vec2 | Die Nah- und Fern-Clip-Werte der Kamera. In den Beispielen auf dieser Seite ist dies (10, 10000) . Kann nur gelesen werden. |
PUNKT_GRÖSSE | Float | Nur 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. |
POSITION | vec4 | Wie 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.vert | Ergebnis |
---|---|
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-Typ | Shader-Typ | Hinweise |
---|---|---|
real, int, bool | float, int, bool | |
Farbe | vec4 | sRGB zu linear Konvertierung wird implizit durchgeführt |
vector2d | vec2 | |
vector3d | vec3 | |
vector4d | vec4 | |
matrix4x4 | matte4 | |
Quaternion | vec4 | skalarer Wert ist w |
rekt | vec4 | |
Punkt, Größe | vec2 | |
TextureInput | Probenehmer2D |
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.vert | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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üsselwort | Typ | Beschreibung |
---|---|---|
BASE_COLOR | vec4 | Die 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_COLOR | vec3 | Die Farbe der Selbstillumination. Entspricht PrincipledMaterial::emissiveFactor. Der Standardwert ist (0.0, 0.0, 0.0) . |
METALNESS | Float | Metalness Wert im Bereich 0-1. Standardwert 0, was bedeutet, dass das Material dielektrisch (nicht metallisch) ist. |
ROUGHNESS | Fließkomma | Roughness Wert im Bereich von 0-1. Der Standardwert ist 0. Größere Werte machen spiegelnde Glanzlichter weicher und verwischen Reflexionen. |
SPIEGELNDER_BETRAG | Fließkommazahl | The 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. |
NORMAL | vec3 | Die interpolierte Normale im Weltraum, angepasst für Doppelseitigkeit, wenn Face Culling deaktiviert ist. Nur lesen. |
UV0 | vec2 | Die interpolierten Texturkoordinaten. Nur Lesen. |
VAR_WELT_POSITION | vec3 | Interpolierte Scheitelpunktposition im Weltraum. Nur lesen. |
Färben wir die Grundfarbe des Würfels rot:
Änderung in main.qml, material.frag | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.qml | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.png | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.qml | Ergebnis |
---|---|
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öhenkarte | Normap-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.qml | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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.frag | Ergebnis |
---|---|
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:
Name | Typ | Beschreibung |
---|---|---|
INPUT | sampler2D oder sampler2DArray | Der Sampler für die Eingabetextur. Ein Effekt wird diese typischerweise mit INPUT_UV sampeln. |
EINGABE_UV | vec2 | UV-Koordinaten für das Sampling INPUT . |
INPUT_SIZE | vec2 | Die Größe der INPUT Textur, in Pixeln. Dies ist eine praktische Alternative zum Aufruf von textureSize(). |
AUSGABE_GRÖSSE | vec2 | Die 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_TEXTUR | sampler2D | Tiefentextur 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.qml | Ergebnis |
---|---|
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.qml | effect.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.qml | effect2.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.