Sur cette page

Matériaux, effets, géométrie et données de texture programmables

Bien que les matériaux intégrés de Qt Quick 3D, DefaultMaterial et PrincipledMaterial, permettent un large degré de personnalisation via leurs propriétés, ils n'offrent pas de possibilité de programmation au niveau du vertex et du fragment shader. Pour ce faire, le type CustomMaterial est fourni.

Un modèle avec PrincipledMaterialAvec un CustomMaterial transformant les sommets

Théière rendue avec du matériel standard

Théière dont les vertices sont transformés par un matériau personnalisé

Les effets de post-traitement, où une ou plusieurs passes de traitement sur le tampon de couleur sont effectuées, en prenant éventuellement en compte le tampon de profondeur, avant que la sortie de View3D ne soit transmise à Qt Quick, existent également en deux variétés :

  • les étapes de post-traitement intégrées qui peuvent être configurées via ExtendedSceneEnvironment, telles que la lueur/le flou, la profondeur de champ, la vignette, le reflet de l'objectif,
  • custom les effets mis en œuvre par l'application sous la forme d'un code de nuanceur de fragment et une spécification des passes de traitement dans un objet Effect.

Dans la pratique, il existe une troisième catégorie d'effets de post-traitement : les effets 2D mis en œuvre via Qt Quick, qui opèrent sur la sortie de l'objet View3D sans aucune intervention du moteur de rendu 3D. Par exemple, pour appliquer un flou à un élément View3D, l'approche la plus simple consiste à utiliser les fonctions existantes de Qt Quick, telles que MultiEffect. Le système de post-traitement 3D est utile pour les effets complexes qui impliquent des concepts de scène 3D tels que le tampon de profondeur ou la texture de l'écran, ou qui doivent traiter la cartographie des tons HDR ou qui nécessitent plusieurs passages avec des tampons intermédiaires, etc. Les effets 2D simples qui ne nécessitent aucune connaissance de la scène 3D et du moteur de rendu peuvent toujours être mis en œuvre à l'aide de ShaderEffect ou MultiEffect.

Scène sans effetLa même scène avec un effet de post-traitement personnalisé appliqué

Scène avec sphère, cône et cube sans effet de post-traitement

Scène avec géométrie déformée par un effet de post-traitement personnalisé

Outre les matériaux programmables et le post-traitement, il existe deux types de données qui sont normalement fournies sous forme de fichiers (fichiers.mesh ou images telles que .png) :

  • les données de vertex, y compris la géométrie du maillage à rendre, les coordonnées de texture, les normales, les couleurs et d'autres données,
  • le contenu des textures qui sont ensuite utilisées comme cartes de texture pour les objets rendus, ou utilisées avec la boîte à ciel ou l'éclairage basé sur l'image.

Si elles le souhaitent, les applications peuvent fournir ces données à partir de C++ sous la forme d'un QByteArray. Ces données peuvent également être modifiées au fil du temps, ce qui permet de générer de manière procédurale et de modifier ultérieurement les données d'un Model ou d'un Texture.

Une grille, rendue en spécifiant les données des sommets de manière dynamique à partir de C++.Un cube texturé avec des données d'image générées à partir de C++.

Grille générée à partir de données géométriques personnalisées

Cube avec texture dégradée générée de manière procédurale

Ces quatre approches de la personnalisation et de la dynamisation des matériaux, des effets, de la géométrie et des textures permettent de programmer l'ombrage et de générer de manière procédurale les données que les nuanceurs reçoivent en entrée. Les sections suivantes donnent un aperçu de ces fonctionnalités. La référence complète est disponible dans les pages de documentation des types respectifs :

Programmabilité des matériaux

Prenons une scène avec un cube, et commençons par les valeurs par défaut PrincipledMaterial et 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 { }
         }
    }
}

Les deux conduisent exactement au même résultat, parce qu'un CustomMaterial est effectivement un PrincipledMaterial, lorsqu'aucun code de vertex ou de fragment shader ne lui est ajouté.

Cube blanc avec matériau par défaut

Remarque : les propriétés telles que baseColor, metalness, baseColorMap, et bien d'autres, n'ont pas de propriétés équivalentes dans le type CustomMaterial QML. C'est une question de conception : la personnalisation du matériau se fait par le biais du code du shader, et non pas en fournissant simplement quelques valeurs fixes.

Notre premier vertex shader

Ajoutons un extrait de vertex shader personnalisé. Pour ce faire, nous référençons un fichier dans la propriété vertexShader. L'approche sera la même pour les fragment shaders. Ces références fonctionnent comme Image.source ou ShaderEffect.vertexShader: ce sont des URL locales ou qrc, et un chemin relatif est traité par rapport à l'emplacement du fichier .qml. L'approche courante consiste donc à placer les fichiers .vert et .frag dans le système de ressources de Qt XML (qt_add_resources lorsque l'on utilise CMake) et à les référencer à l'aide d'un chemin relatif.

Dans Qt 6.0, les chaînes de shaders en ligne ne sont plus prises en charge, ni dans Qt Quick ni dans Qt Quick 3D. (Cependant, en raison de leur nature intrinsèquement dynamique, les matériaux personnalisés et les effets de post-traitement dans Qt Quick 3D fournissent toujours des extraits de shaders sous forme de source dans les fichiers référencés. Il s'agit d'une différence par rapport à ShaderEffect où les shaders sont complets, sans autre modification par le moteur, et sont donc censés être fournis en tant que packs de shaders pré-conditionnés .qsb.

Remarque : dans Qt Quick 3D, les URL ne peuvent faire référence qu'à des ressources locales. Les schémas pour le contenu distant ne sont pas pris en charge.

Remarque : le langage d'ombrage utilisé est GLSL, compatible avec Vulkan. Les fichiers .vert et .frag ne sont pas des shaders complets en soi, c'est pourquoi ils sont souvent appelés snippets. C'est pourquoi il n'y a pas de blocs d'uniformes, de variables d'entrée et de sortie ou d'uniformes d'échantillonneurs fournis directement par ces extraits. Le moteur 3D de Qt Quick les modifiera en fonction des besoins.

Changement dans main.qml, material.vertRésultat
materials: CustomMaterial {
    vertexShader: "material.vert"
}
void MAIN()
{
}

Cube blanc avec matériau par défaut

Un snippet de vertex ou de fragment shader personnalisé est censé fournir une ou plusieurs fonctions avec des noms prédéfinis, tels que MAIN, DIRECTIONAL_LIGHT, POINT_LIGHT, SPOT_LIGHT, AMBIENT_LIGHT, SPECULAR_LIGHT. Pour l'instant, concentrons-nous sur MAIN.

Comme on le voit ici, le résultat final avec une MAIN() vide est exactement le même que précédemment.

Avant de rendre les choses plus intéressantes, voyons un aperçu des mots-clés spéciaux les plus couramment utilisés dans les snippets de vertex shaders personnalisés. Cette liste n'est pas exhaustive. Pour une référence complète, consultez la page CustomMaterial.

Mot-cléType de motDescription du mot-clé
MAINvoid MAIN() est le point d'entrée. Cette fonction doit toujours être présente dans un extrait de vertex shader personnalisé, il n'y a pas d'intérêt à en fournir une autrement.
VERTEXvec3La position du vertex que le nuanceur reçoit en entrée. Un cas d'utilisation courant pour les vertex shaders dans les matériaux personnalisés est de changer (déplacer) les valeurs x, y ou z de ce vecteur, en assignant simplement une valeur à l'ensemble du vecteur, ou à certains de ses composants.
NORMALvec3La normale du sommet à partir des données du maillage d'entrée, ou tous les zéros s'il n'y a pas de normales fournies. Comme pour VERTEX, le shader est libre de modifier la valeur comme il l'entend. La valeur modifiée est ensuite utilisée par le reste du pipeline, y compris les calculs d'éclairage dans la phase de fragmentation.
UV0vec2Le premier ensemble de coordonnées de texture provenant des données de maillage d'entrée, ou tous les zéros si aucune valeur UV n'a été fournie. Comme pour VERTEX et NORMAL, la valeur peut être modifiée.
MATRICE_DE_PROJECTION_DU_MODÈLEmat4La matrice de projection modèle-vue. Pour unifier le comportement quelle que soit l'API graphique utilisée pour le rendu, toutes les données de vertex et les matrices de transformation suivent les conventions OpenGL à ce niveau. (Axe Y pointant vers le haut, matrice de projection compatible OpenGL) Lecture seule.
MODEL_MATRIXmat4Matrice du modèle (monde). En lecture seule.
NORMAL_MATRIXmat3L'inverse transposé de la tranche supérieure gauche 3x3 de la matrice du modèle. En lecture seule.
CAMERA_POSITIONvec3Position de la caméra dans l'espace mondial. Dans les exemples de cette page, il s'agit de (0, 0, 600). Lecture uniquement.
CAMERA_DIRECTIONvec3Le vecteur de direction de la caméra. Dans les exemples de cette page, il s'agit de (0, 0, -1). Lecture uniquement.
CAMERA_PROPERTIESvec2Les valeurs de clip proche et lointain de la caméra. Dans les exemples de cette page, il s'agit de (10, 10000). Lecture uniquement.
POINT_SIZEfloatNe concerne que le rendu avec une topologie de points, par exemple parce que custom geometry fournit une telle géométrie pour le maillage. L'écriture de cette valeur équivaut au réglage de pointSize on a PrincipledMaterial.
POSITIONvec4Comme gl_Position. Lorsqu'elle n'est pas présente, une instruction d'affectation par défaut est générée automatiquement à l'aide de MODELVIEWPROJECTION_MATRIX et VERTEX. C'est pourquoi une MAIN() vide est fonctionnelle et, dans la plupart des cas, il n'est pas nécessaire de lui attribuer une valeur personnalisée.

Créons un matériau personnalisé qui déplace les sommets selon un certain modèle. Pour le rendre plus intéressant, nous avons quelques propriétés QML animées, dont les valeurs sont exposées en tant qu'uniformes dans le code du shader. (pour être précis, la plupart des propriétés vont être mappées aux membres d'un bloc uniforme, soutenu par un tampon uniforme au moment de l'exécution, mais Qt Quick 3D rend commodément de tels détails transparents pour l'auteur du matériau personnalisé).

Changement dans main.qml, material.vertRésultat
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;
}

Uniformes à partir des propriétés QML

Les propriétés personnalisées de l'objet CustomMaterial sont associées à des uniformes. Dans l'exemple ci-dessus, il s'agit de uAmplitude et uTime. Chaque fois que les valeurs changent, la valeur mise à jour devient visible dans le shader. Ce concept vous est peut-être déjà familier grâce à ShaderEffect.

Le nom de la propriété QML et de la variable GLSL doit correspondre. Il n'y a pas de déclaration séparée dans le code du nuanceur pour les différents uniformes. Le nom de la propriété QML peut être utilisé tel quel. C'est pourquoi l'exemple ci-dessus peut simplement faire référence à uTime et uAmplitude dans l'extrait de vertex shader sans aucune déclaration préalable.

Le tableau suivant indique comment les types sont mis en correspondance :

Type QMLType de nuanceurNotes
real, int, boolfloat, int, bool
couleurvec4la conversion sRGB vers linéaire est effectuée implicitement
vector2dvec2
vector3dvec3
vector4dvec4
matrix4x4mat4
quaternionvec4la valeur scalaire est w
rectvec4
point, taillevec2
TextureInputsampler2D

Amélioration de l'exemple

Avant d'aller plus loin, améliorons l'exemple. En ajoutant un maillage rectangulaire rotatif et en faisant en sorte que le site DirectionalLight projette des ombres, nous pouvons vérifier que la modification des sommets du cube est correctement reflétée dans toutes les passes de rendu, y compris les cartes d'ombres. Pour obtenir une ombre visible, la lumière est maintenant placée un peu plus haut sur l'axe Y, et une rotation est appliquée pour qu'elle pointe partiellement vers le bas. (comme il s'agit d'une lumière directional, la rotation est importante)

main.qml, material.vertRésultat
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;
}

Ajout d'un fragment shader

De nombreux matériaux personnalisés nécessiteront également un fragment shader. En fait, beaucoup ne voudront qu'un fragment shader. S'il n'y a pas de données supplémentaires à transmettre de l'étape vertex à l'étape fragment, et que la transformation vertex par défaut est suffisante, la définition de la propriété vertexShader peut être omise de la page CustomMaterial.

Changement dans main.qml, material.fragRésultat
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
}

Cube blanc avec fragment shader vide

Notre premier fragment shader contient une fonction MAIN() vide. Ce n'est pas différent de ne pas spécifier de fragment shader du tout : ce que nous obtenons ressemble à ce que nous obtenons avec un PrincipledMaterial par défaut.

Examinons quelques-uns des mots-clés couramment utilisés dans les nuanceurs de fragment. Il ne s'agit pas d'une liste exhaustive, reportez-vous à la documentation de CustomMaterial pour une référence complète. Beaucoup d'entre eux sont en lecture-écriture, ce qui signifie qu'ils ont une valeur par défaut, mais le nuanceur peut, et voudra souvent, leur attribuer une valeur différente.

Comme leur nom l'indique, nombre d'entre elles correspondent à des propriétés PrincipledMaterial portant le même nom, avec la même signification et la même sémantique, suivant le modèle de matériau à rugosité métallique. C'est à l'implémentation du matériau personnalisé de décider comment ces valeurs sont calculées : par exemple, une valeur pour BASE_COLOR peut être codée en dur dans le shader, peut être basée sur l'échantillonnage d'une texture, ou peut être calculée sur la base des propriétés QML exposées en tant qu'uniformes ou sur des données interpolées transmises par le vertex shader.

Mot-cléType de mot-cléDescription
BASE_COLORvec4La couleur de base et la valeur alpha. Correspond à PrincipledMaterial::baseColor. La valeur alpha finale du fragment est l'opacité du modèle multipliée par la couleur de base alpha. La valeur par défaut est (1.0, 1.0, 1.0, 1.0).
EMISSIVE_COLORvec3La couleur de l'auto-illumination. Correspond à PrincipledMaterial::emissiveFactor. La valeur par défaut est (0.0, 0.0, 0.0).
METALNESSfloatMetalness valeur comprise entre 0 et 1. La valeur par défaut est 0, ce qui signifie que le matériau est diélectrique (non métallique).
ROUGHNESSfloatRoughness valeur comprise entre 0 et 1. La valeur par défaut est 0. Les valeurs plus élevées adoucissent les reflets spéculaires et brouillent les réflexions.
QUANTITÉ SPÉCULAIREvaleur flottanteThe strength of specularity compris entre 0 et 1. La valeur par défaut est 0.5. Pour les objets métalliques dont la valeur metalness est réglée sur 1, cette valeur n'a aucun effet. Lorsque les valeurs de SPECULAR_AMOUNT et METALNESS sont supérieures à 0 mais inférieures à 1, le résultat est un mélange entre les deux modèles de matériaux.
NORMALvec3La normale interpolée dans l'espace mondial, ajustée pour les deux côtés lorsque l'élimination des faces est désactivée. En lecture seule.
UV0vec2Les coordonnées interpolées de la texture. Lecture uniquement.
VAR_WORLD_POSITIONvec3Position interpolée du sommet dans l'espace mondial. En lecture seule.

Rendons la couleur de base du cube rouge :

Changement dans main.qml, material.fragRésultat
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 1.0);
}

Cube rouge avec couleur de base personnalisée

Renforçons maintenant un peu le niveau d'auto-illumination :

Changement dans main.qml, material.fragRésultat
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(1.0, 0.0, 0.0, 1.0);
    EMISSIVE_COLOR = vec3(0.4);
}

Cube rouge vif avec couleur émissive

Au lieu d'avoir des valeurs codées en dur dans le shader, nous pourrions aussi utiliser les propriétés QML exposées comme des uniformes, même animés :

Changement dans main.qml, material.fragRésultat
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);
}

Faisons quelque chose de moins trivial, quelque chose qui n'est pas implémentable avec PrincipledMaterial et ses propriétés standard intégrées. Le matériau suivant visualise les coordonnées UV de la texture du cube. U va de 0 à 1, donc du noir au rouge, tandis que V va également de 0 à 1, du noir au vert.

Changement dans main.qml, material.fragRésultat
materials: CustomMaterial {
    fragmentShader: "material.frag"
}
void MAIN()
{
    BASE_COLOR = vec4(UV0, 0.0, 1.0);
}

Cube représentant les coordonnées UV sous la forme d'un dégradé de couleurs rouge et vert

Tant que nous y sommes, pourquoi ne pas visualiser également les normales, cette fois sur une sphère. Comme pour les UV, si un snippet de vertex shader personnalisé devait modifier la valeur de NORMAL, la valeur interpolée par fragment dans le fragment shader, également exposée sous le nom de NORMAL, refléterait ces ajustements.

Modification dans main.qml, material.fragRésultat
Model {
    source: "#Sphere"
    scale: Qt.vector3d(2, 2, 2)
    materials: CustomMaterial {
        fragmentShader: "material.frag"
    }
}
void MAIN()
{
    BASE_COLOR = vec4(NORMAL, 1.0);
}

Sphère visualisant les normales de surface sous forme de couleurs RVB

Couleurs

Passons à un modèle de théière pour un moment, faisons du matériau un mélange de métallique et de diélectrique, et essayons de lui donner une couleur de base verte. La valeur green QColor correspond à (0, 128, 0), ce qui nous a permis de faire notre première tentative :

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;
}

Théière verte avec valeurs de couleur RVB directes

Cela ne semble pas tout à fait correct. Comparez avec la deuxième approche :

Changement dans main.qml, material.fragRésultat
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;
}

Théière verte avec propriété de couleur QML et conversion sRGB

En passant à PrincipledMaterial, nous pouvons confirmer qu'en réglant PrincipledMaterial::baseColor sur "green" et en suivant la métallicité et d'autres propriétés, le résultat est identique à notre deuxième approche :

Changement dans main.qmlRésultat
materials: PrincipledMaterial {
    baseColor: "green"
    metalness: 0.6
    specularAmount: 0.4
    roughness: 0.4
}

Théière verte avec matériel standard montrant une couleur correcte

Si le type de la propriété uColor était modifié en vector4d, ou tout autre type que color, les résultats changeraient soudainement et deviendraient identiques à notre première approche.

Comment cela se fait-il ?

La réponse réside dans la conversion de sRGB en linéaire qui est effectuée implicitement pour les propriétés de couleur de DefaultMaterial, PrincipledMaterial, et également pour les propriétés personnalisées avec un type color dans CustomMaterial. Cette conversion n'est pas effectuée pour toute autre valeur, donc si le shader code en dur une valeur de couleur, ou la base sur une propriété QML avec un type différent de color, ce sera au shader d'effectuer la linéarisation dans le cas où la valeur source était dans l'espace colorimétrique sRGB. La conversion en linéaire est importante car Qt Quick 3D effectue tonemapping sur les résultats de l'ombrage de fragments, et ce processus suppose des valeurs dans l'espace sRGB comme entrée.

Les constantes intégrées QColor, telles que "green", sont toutes données dans l'espace sRGB. Par conséquent, l'affectation de vec4(0.0, 0.5, 0.0, 1.0) à BASE_COLOR lors de la première tentative est insuffisante si nous voulons obtenir un résultat qui corresponde à une valeur RVB (0, 128, 0) dans l'espace sRGB. Voir la documentation BASE_COLOR dans CustomMaterial pour une formule de linéarisation de ces valeurs de couleur. Il en va de même pour les valeurs de couleur obtenues par échantillonnage de textures : si les données de l'image source ne sont pas dans l'espace colorimétrique sRGB, une conversion est nécessaire (à moins que tonemapping ne soit désactivé).

Mélange

Il ne suffit pas d'écrire une valeur inférieure à 1.0 dans BASE_COLOR.a si l'on veut obtenir un mélange alpha. Ces matériaux modifieront très souvent les valeurs des propriétés sourceBlend et destinationBlend pour obtenir les résultats souhaités.

N'oubliez pas non plus que la valeur alpha combinée est la valeur Node opacity multipliée par l'alpha du matériau.

Pour visualiser, utilisons un shader qui affecte le rouge avec l'alpha 0.5 à BASE_COLOR:

main.qml, material.fragRésultat
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);
}

Trois cubes montrant les variations d'opacité du mode de fusion

Le premier cube écrit 0,5 à la valeur alpha de la couleur mais n'apporte pas de résultats visibles puisque l'alpha blending n'est pas activé. Le deuxième cube active l'alpha blending via les propriétés CustomMaterial. Le troisième attribue également une opacité de 0,5 au modèle, ce qui signifie que l'opacité effective est de 0,25.

Transmission de données entre le vertex et le fragment shader

Calculer une valeur par sommet (par exemple, en supposant un seul triangle, pour les 3 coins du triangle), puis la transmettre à l'étape de fragmentation, où pour chaque fragment (par exemple, chaque fragment couvert par le triangle tramé) une valeur interpolée est rendue accessible. Dans les snippets de shaders matériels personnalisés, cela est rendu possible par le mot-clé VARYING. Celui-ci fournit une syntaxe similaire à GLSL 120 et GLSL ES 100, mais fonctionne quelle que soit l'API graphique utilisée au moment de l'exécution. Le moteur se chargera de réécrire la déclaration variable comme il se doit.

Voyons à quoi ressemble l'échantillonnage de texture classique avec les coordonnées UV. Les textures seront abordées dans une prochaine section, pour l'instant concentrons-nous sur la façon dont nous obtenons les coordonnées UV qui peuvent être passées à la fonction texture() dans le shader.

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.pngRésultat

Texture du logo Qt utilisée comme donnée source

Cube avec texture du logo Qt appliquée aux faces

Notez que les déclarations VARYING. Le nom et le type doivent correspondre, uv dans le fragment shader exposera les coordonnées UV interpolées pour le fragment actuel.

Tout autre type de données peut être transmis à l'étape du fragment de la même manière. Il convient de noter que dans de nombreux cas, il n'est pas nécessaire de configurer les propres variations du matériau, car il existe des composants intégrés qui couvrent de nombreux besoins typiques. Il s'agit notamment de créer les normales (interpolées), les UV, la position du monde (VAR_WORLD_POSITION) ou le vecteur pointant vers la caméra (VIEW_VECTOR).

L'exemple ci-dessus peut en fait être simplifié comme suit, car UV0 est automatiquement disponible à l'étape du fragment :

Changement dans main.qml, material.fragRésultat
materials: CustomMaterial {
    fragmentShader: "material.frag"
    property TextureInput someTextureMap: TextureInput {
        texture: Texture {
        source: "qt_logo_rect.png"
    }
}
void MAIN()
{
    BASE_COLOR = texture(someTextureMap, UV0);
}

Cube avec texture du logo Qt appliquée aux faces

Pour désactiver l'interpolation d'une variable, utilisez le mot-clé flat dans l'extrait de nuanceur de sommets et de fragments. Par exemple, dans l'extrait de shader de vertex et de fragment :

VARYING flat vec2 v;

Textures

Une carte CustomMaterial n'a pas de cartes de texture intégrées, ce qui signifie qu'il n'y a pas d'équivalent, par exemple, de PrincipledMaterial::baseColorMap. La raison en est que l'implémentation de la même chose est souvent triviale, tout en offrant beaucoup plus de flexibilité que ce que DefaultMaterial et PrincipledMaterial ont intégré. Outre le simple échantillonnage d'une texture, les snippets de fragment shader personnalisés sont libres de combiner et de mélanger des données provenant de diverses sources lorsqu'ils calculent les valeurs qu'ils attribuent à BASE_COLOR, EMISSIVE_COLOR, ROUGHNESS, etc. Ils peuvent baser ces calculs sur les données fournies par les propriétés QML, les données interpolées envoyées par l'étage des vertex, les valeurs extraites de l'échantillonnage des textures et les valeurs codées en dur.

Comme le montre l'exemple précédent, l'exposition d'une texture au vertex, au fragment ou aux deux shaders est très similaire aux valeurs uniformes scalaires et vectorielles : une propriété QML de type TextureInput sera automatiquement associée à une sampler2D dans le code du shader. Comme toujours, il n'est pas nécessaire de déclarer cet échantillonneur dans le code du nuanceur.

Un TextureInput fait référence à un Texture, avec une propriété enabled supplémentaire. Les données d'un Texture peuvent être obtenues de trois manières différentes : from an image file, from a texture with live Qt Quick contentou can be provided from C++ via QQuick3DTextureData.

Remarque : en ce qui concerne les propriétés de Texture, les propriétés relatives à la source, au tuilage et au filtrage sont les seules qui sont prises en compte implicitement avec les matériaux personnalisés, le reste (comme les transformations UV) étant à la charge des shaders personnalisés qui doivent les mettre en œuvre comme ils l'entendent.

Voyons un exemple où un modèle, une sphère dans ce cas, est texturé en utilisant du contenu live Qt Quick:

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;
}

Ici, le sous-arbre 2D (Rectangle avec deux enfants : un autre Rectangle et le Texte) est rendu dans une texture 2D 512x512 à chaque fois que cette mini-scène change. La texture est ensuite exposée au matériau personnalisé sous le nom de someTextureMap.

Notez l'inversion de la coordonnée V dans le shader. Comme indiqué ci-dessus, les matériaux personnalisés, qui sont entièrement programmables au niveau des shaders, n'offrent pas les caractéristiques "fixes" de Texture et PrincipledMaterial. Cela signifie que toute transformation des coordonnées UV devra être appliquée par le shader. Ici, nous savons que la texture est générée via Texture::sourceItem et que V doit donc être retourné pour obtenir quelque chose qui corresponde au jeu d'UV du maillage que nous utilisons.

Ce que cet exemple montre est également possible avec PrincipledMaterial. Rendons-le plus intéressant en ajoutant un simple effet d'embossage :

material.fragRésultat
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);
}

Avec les fonctionnalités couvertes jusqu'à présent, un large éventail de possibilités est ouvert pour créer des matériaux qui nuancent les maillages de manière visuellement impressionnante. Pour terminer la visite de base, regardons un exemple qui applique des cartes de hauteur et de normalité à un maillage plan. (un fichier .mesh dédié est utilisé ici car le fichier intégré #Rectangle n'a pas assez de subdivisions) Pour de meilleurs résultats d'éclairage, nous utiliserons un éclairage basé sur l'image avec une image HDR à 360 degrés. L'image est également définie comme skybox pour rendre plus clair ce qui se passe.

Commençons par un fichier vide CustomMaterial:

main.qmlRésultat
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 { }
        }
    }
}

Plan avec paysage hivernal à partir de données de texture personnalisées

Maintenant, créons quelques shaders qui appliquent une carte de hauteur et de normalité au maillage :

Carte de hauteurCarte de normalité

Texture de la carte d'altitude en niveaux de gris

Plan avec carte de hauteur appliquée comme déplacement

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));
}
Changement dans main.qmlRésultat
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" }
    }
}

Plan avec carte des normales montrant les détails de la surface

Note : L'objet WasdController peut être extrêmement utile pendant le développement et le dépannage car il permet de naviguer et de regarder autour de la scène avec le clavier et la souris d'une manière familière. Disposer d'une caméra contrôlée par le site WasdController est aussi simple que d'utiliser une souris ou un clavier pour naviguer dans la scène :

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

Profondeur et textures d'écran

Lorsqu'un snippet de shader personnalisé utilise les mots-clés DEPTH_TEXTURE ou SCREEN_TEXTURE, il choisit de générer les textures correspondantes dans une passe de rendu séparée, ce qui n'est pas nécessairement une opération bon marché, mais permet de mettre en œuvre une variété de techniques, telles que la réfraction pour les matériaux ressemblant à du verre.

DEPTH_TEXTURE est un sampler2D qui permet d'échantillonner une texture avec le contenu du tampon de profondeur avec tous les objets opaque de la scène rendus. De même, SCREEN_TEXTURE est un sampler2D qui permet d'échantillonner une texture contenant le contenu de la scène à l'exclusion de tout matériau transparent ou de tout matériau utilisant également la SCREEN_TEXTURE. La texture peut être utilisée pour les matériaux qui nécessitent le contenu du framebuffer dans lequel ils sont rendus. La texture SCREEN_TEXTURE utilise le même mode transparent que la texture View3D. La taille de ces textures correspond à la taille de View3D en pixels.

Faisons une démonstration simple en visualisant le contenu du tampon de profondeur via DEPTH_TEXTURE. L'adresse far clip value de la caméra est ici réduite de 10000 à 2000 par défaut, afin d'avoir une plage plus petite, et donc d'avoir les différences de valeurs de profondeur visualisées plus évidentes. Le résultat est un rectangle qui visualise le tampon de profondeur de la scène sur sa surface.

main.qml, material.fragRésultat
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);
}

Notez que le cylindre n'est pas présent dans DEPTH_TEXTURE en raison de son recours à la semi-transparence, ce qui le place dans une catégorie différente de celle des autres objets qui sont tous opaques. Ces objets n'écrivent pas dans le tampon de profondeur, bien qu'ils testent les valeurs de profondeur écrites par les objets opaques, et s'appuient sur un rendu dans l'ordre inverse. Ils ne sont donc pas non plus présents dans DEPTH_TEXTURE.

Que se passe-t-il si nous passons le shader à l'échantillon SCREEN_TEXTURE à la place ?

material.fragRésultat
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;
}

Scène montrant une texture d'écran avec effet de distorsion

Ici, le rectangle est texturé avec SCREEN_TEXTURE, tout en remplaçant les pixels transparents par du violet.

Fonctions du processeur de lumière

Une fonctionnalité avancée de CustomMaterial est la possibilité de définir des fonctions dans le nuanceur de fragment qui réimplémentent les équations d'éclairage utilisées pour calculer la couleur du fragment. Une fonction de processeur de lumière, lorsqu'elle existe, est appelée une fois pour chaque lumière de la scène, pour chaque fragment. Il existe une fonction dédiée aux différents types de lumière, ainsi qu'à la contribution ambiante et spéculaire. Lorsqu'il n'y a pas de fonction de traitement de la lumière correspondante, les calculs standard sont utilisés, comme le ferait un site PrincipledMaterial. Lorsqu'un processeur de lumière est présent, mais que le corps de la fonction est vide, cela signifie qu'il n'y aura pas de contribution d'un type de lumière donné dans la scène.

Reportez-vous à la documentation CustomMaterial pour plus de détails sur les fonctions telles que DIRECTIONAL_LIGHT, POINT_LIGHT, SPOT_LIGHT, AMBIENT_LIGHT et SPECULAR_LIGHT.

Matériaux personnalisés non ombrés

Il existe un autre type de CustomMaterial: les matériaux personnalisés unshaded. Tous les exemples présentés jusqu'à présent utilisaient des matériaux personnalisés shaded, la propriété shadingMode étant laissée à sa valeur par défaut CustomMaterial.Shaded.

Que se passe-t-il si nous attribuons à cette propriété la valeur CustomMaterial.Unshaded ?

Tout d'abord, les mots-clés tels que BASE_COLOR, EMISSIVE_COLOR, METALNESS, etc. n'ont plus l'effet escompté. En effet, un matériau non ombré, comme son nom l'indique, n'est pas automatiquement modifié par une grande partie du code d'ombrage standard, ignorant ainsi les lumières, l'éclairage basé sur l'image, les ombres et l'occlusion ambiante dans la scène. Au contraire, un matériau non ombré donne un contrôle total au shader via le mot-clé FRAGCOLOR. Ceci est similaire à gl_FragColor : la couleur assignée à FRAGCOLOR est le résultat et la couleur finale du fragment, sans aucun autre ajustement par Qt Quick 3D.

main.qml, material.frag, material2.fragRésultat
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);
}

Cube et cylindre avec un matériau personnalisé non ombré

Remarquez que le cylindre de droite ignore le site DirectionalLight de la scène. Son ombrage ne connaît pas l'éclairage de la scène, la couleur finale du fragment est entièrement blanche.

Le vertex shader d'un matériau non ombré dispose toujours des entrées typiques : VERTEX NORMAL , MODELVIEWPROJECTION_MATRIX, etc. et peut écrire sur POSITION. Le nuanceur de fragments ne dispose cependant plus des mêmes commodités : NORMAL, UV0, ou VAR_WORLD_POSITION ne sont pas disponibles dans le nuanceur de fragments d'un matériau non ombré. C'est plutôt au code du nuanceur de calculer et de transmettre à l'aide de VARYING tout ce dont il a besoin pour déterminer la couleur finale du fragment.

Examinons un exemple qui comporte à la fois un nuanceur de sommets et un nuanceur de fragments. La position modifiée du sommet est transmise au nuanceur de fragment, avec une valeur interpolée mise à la disposition de chaque fragment.

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);
}

Les matériaux non nuancés sont utiles lorsqu'il n'est pas nécessaire ou souhaitable d'interagir avec l'éclairage de la scène et que le matériau doit avoir un contrôle total sur la couleur finale du fragment. Remarquez que l'exemple ci-dessus n'a ni DirectionalLight ni aucune autre lumière, mais que la sphère avec le matériau personnalisé apparaît comme prévu.

Remarque : un matériau non ombré qui ne possède qu'un extrait de vertex shader, mais qui ne spécifie pas la propriété fragmentShader, sera toujours fonctionnel, mais les résultats seront les mêmes que si le shadingMode était réglé sur Shaded (ombré). Il n'est donc pas utile de changer de shadingMode pour les matériaux qui n'ont qu'un vertex shader.

Programmabilité des effets

Les effets de post-traitement appliquent un ou plusieurs nuanceurs de fragment au résultat d'un rendu View3D. La sortie de ces nuanceurs de fragment est alors affichée à la place des résultats du rendu original. Le concept est très similaire à celui des effets Qt Quick's ShaderEffect et ShaderEffectSource.

Remarque : les effets de post-traitement ne sont disponibles que lorsque le paramètre renderMode de View3D est réglé sur View3D. hors écran.

Des snippets de vertex shader personnalisés peuvent également être spécifiés pour un effet, mais leur utilité est limitée et ils devraient donc être utilisés relativement rarement. Le vertex d'entrée d'un effet de post-traitement est un quad (soit deux triangles, soit une bande de triangles), la transformation ou le déplacement des vertex n'est souvent pas utile. Il peut cependant être utile d'avoir un nuanceur de sommets pour calculer et transmettre des données au nuanceur de fragments à l'aide du mot-clé VARYING. Comme d'habitude, le fragment shader recevra alors une valeur interpolée basée sur les coordonnées actuelles du fragment.

La syntaxe des extraits de shaders associés à un Effect est identique à celle des shaders d'un CustomMaterial non ombré. En ce qui concerne les mots-clés spéciaux intégrés, VARYING, MAIN, FRAGCOLOR (fragment shader uniquement), POSITION (vertex shader uniquement), VERTEX (vertex shader uniquement) et MODELVIEWPROJECTION_MATRIX fonctionnent de la même manière que CustomMaterial.

Les mots-clés spéciaux les plus importants pour les nuanceurs de fragment Effect sont les suivants :

NomType de mot-cléDescription
INPUTsampler2D ou sampler2DArrayL'échantillonneur pour la texture d'entrée. Un effet échantillonnera généralement cette texture à l'aide de INPUT_UV.
INPUT_UVvec2Coordonnées UV pour l'échantillonnage INPUT.
INPUT_SIZEvec2La taille de la texture INPUT, en pixels. C'est une alternative pratique à l'appel de textureSize().
OUTPUT_SIZEvec2La taille de la texture de sortie, en pixels. Dans la plupart des cas, elle est égale à INPUT_SIZE, mais un effet à plusieurs passages peut avoir des passages qui produisent des textures intermédiaires de tailles différentes.
DEPTH_TEXTUREsampler2DTexture de profondeur avec le contenu du tampon de profondeur avec les objets opaques de la scène. Comme pour CustomMaterial, la présence de ce mot-clé dans le shader déclenche la génération automatique de la texture de profondeur.

Remarque : lorsque le rendu multivue est activé, la texture d'entrée est un tableau de textures 2D. Les fonctions GLSL telles que texture() et textureSize() prennent/renvoient respectivement un vec3/ivec3. Utilisez VIEW_INDEX pour la couche. Dans les applications VR/AR qui souhaitent fonctionner avec et sans rendu multi-vues, l'approche portable consiste à écrire le code du shader de cette manière :

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

Un effet de post-traitement

Commençons par une scène simple, cette fois-ci avec quelques objets supplémentaires, dont un rectangle texturé qui utilise une texture en damier comme carte de couleur de base.

main.qmlRésultat
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 { }
        }
    }
}

Scène de référence avec sphère, cône et cube

Appliquons maintenant un effet à l'ensemble de la scène. Plus précisément, à l'objet View3D. Lorsqu'il y a plusieurs objets View3D dans la scène, chacun a son propre SceneEnvironment et donc sa propre chaîne d'effets de post-traitement. Dans l'exemple, il n'y a qu'un seul View3D qui couvre toute la fenêtre.

Changement dans 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;
}

Cet effet simple modifie la valeur du canal de couleur rouge. L'exposition des propriétés QML en tant qu'uniformes fonctionne de la même manière avec les effets qu'avec les matériaux personnalisés. Le shader commence par une ligne qui sera très courante lors de l'écriture de shaders de fragments pour les effets : l'échantillonnage INPUT aux coordonnées UV INPUT_UV. Il effectue ensuite les calculs nécessaires et attribue la couleur finale du fragment à FRAGCOLOR.

De nombreuses propriétés définies dans l'exemple sont au pluriel (effets, passes, shaders). Alors que la syntaxe de la liste [ ] peut être omise lorsqu'il n'y a qu'un seul élément, toutes ces propriétés sont des listes et peuvent contenir plus d'un élément. Pourquoi cela ?

  • effects est une liste, car View3D permet d'enchaîner plusieurs effets. Les effets sont appliqués dans l'ordre dans lequel ils sont ajoutés à la liste. Cela permet d'appliquer facilement deux ou plusieurs effets ensemble à View3D, et est similaire à ce que l'on peut obtenir dans Qt Quick en imbriquant les éléments de ShaderEffect. La texture INPUT de l'effet suivant est toujours une texture qui contient la sortie de l'effet précédent. La sortie du dernier effet dans ce qui est utilisé comme sortie finale de View3D.
  • passes est une liste, car contrairement à ShaderEffect, Effect prend en charge les passages multiples. Un effet à passages multiples est plus puissant que l'enchaînement de plusieurs effets indépendants dans effects: un passage peut produire une texture intermédiaire temporaire, qui peut ensuite être utilisée comme entrée pour les passages suivants, en plus de la texture d'entrée originale de l'effet. Cela permet de créer des effets complexes qui calculent, restituent et mélangent plusieurs textures afin d'obtenir la couleur finale du fragment. Ce cas d'utilisation avancé ne sera pas abordé ici. Reportez-vous à la page de documentation Effect pour plus de détails.
  • shaders est une liste, car un effet peut être associé à un vertex et à un fragment shader.

Enchaînement de plusieurs effets

Voyons un exemple où l'effet de l'exemple précédent est complété par un autre effet similaire à l'effet intégré DistortionSpiral.

Changement dans 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;
}

Maintenant, la question peut-être surprenante : pourquoi cet exemple est-il mauvais ?

Plus précisément, il n'est pas mauvais, mais montre plutôt un modèle qu'il est souvent avantageux d'éviter.

Enchaîner des effets de cette manière peut être utile, mais il est important de garder à l'esprit les implications en termes de performances : faire deux passes de rendu (une pour générer une texture avec le canal de couleur rouge ajusté, puis une autre pour calculer la distorsion) est assez inutile alors qu'une seule suffirait. Si les extraits de shaders de fragments étaient combinés, le même résultat aurait pu être obtenu avec un seul effet.

Définition des données de maillage et de texture en C++

La génération procédurale de données de maillage et de texture suit des étapes similaires :

  • Sous-classe QQuick3DGeometry ou QQuick3DTextureData
  • Définir les données de vertex ou d'image souhaitées lors de la construction en appelant les fonctions membres protégées de la classe de base.
  • Si des changements dynamiques sont nécessaires par la suite, définissez les nouvelles données et appelez update()
  • Une fois l'implémentation terminée, la classe doit être enregistrée pour la rendre visible dans QML
  • Model et les objets Texture dans QML peuvent maintenant utiliser le fournisseur de données de vertex ou d'image personnalisé en définissant la propriété Model::geometry ou Texture::textureData.

Données de sommet personnalisées

Les données de sommet font référence à la séquence de valeurs (généralement float) qui composent un maillage. Au lieu de charger les fichiers .mesh, un fournisseur de géométrie personnalisée est chargé de fournir les mêmes données. Les données de sommet se composent de attributes, telles que la position, les coordonnées de texture (UV) ou les normales. La spécification des attributs décrit le type d'attributs présents, le type de composant (par exemple, un vecteur flottant à trois composants pour la position du sommet, composé de valeurs x, y et z), le décalage auquel ils commencent dans les données fournies, et le stride (l'incrément qui doit être ajouté au décalage pour pointer vers l'élément suivant pour le même attribut).

Cela peut sembler familier si l'on a travaillé avec des API graphiques, comme OpenGL ou Vulkan directement, car la façon dont l'entrée des vertex est spécifiée avec ces API correspond vaguement à ce qu'un fichier .mesh ou une instance QQuick3DGeometry définit.

En outre, la topologie du maillage (type de primitive) doit également être spécifiée. Pour le dessin indexé, les données d'un tampon d'index doivent également être fournies.

Il existe une implémentation intégrée de la géométrie personnalisée : le module QtQuick3D.Helpers comprend un type GridGeometry. Celui-ci permet de rendre une grille dans la scène avec des primitives de ligne, sans avoir à implémenter une sous-classe QQuick3DGeometry personnalisée.

Un autre cas d'utilisation courant est le rendu de points. C'est assez simple à réaliser puisque la spécification des attributs sera minimale : nous fournissons trois flottants (x, y, z) pour chaque sommet, rien d'autre. Une sous-classe de QQuick3DGeometry pourrait implémenter une géométrie composée de 2000 points de la manière suivante :

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);

Combiné à un matériau de

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

le résultat final est similaire à celui-ci (ici vu sous un angle de caméra modifié, avec l'aide de WasdController) :

Géométrie du nuage de points rendue sous forme de points individuels

Remarque : il faut savoir que les tailles de point et les largeurs de ligne autres que 1 peuvent ne pas être prises en charge au moment de l'exécution, en fonction de l'API graphique sous-jacente. Qt n'a aucun contrôle sur ce point. Par conséquent, il peut s'avérer nécessaire de mettre en œuvre des techniques alternatives au lieu de s'appuyer sur le dessin de points et de lignes.

Données de texture personnalisées

Avec les textures, les données à fournir sont structurellement beaucoup plus simples : il s'agit des données brutes des pixels, avec un nombre variable d'octets par pixel, en fonction du format de la texture. Par exemple, une texture RGBA attend quatre octets par pixel, tandis que RGBA16F attend quatre demi-floats par pixel. Cela correspond à ce qu'un site QImage stocke en interne. Toutefois, les textures 3D Qt Quick peuvent avoir des formats dont les données ne peuvent pas être représentées par un QImage. Par exemple, les textures HDR à virgule flottante ou les textures compressées. C'est pourquoi les données de QQuick3DTextureData sont toujours fournies sous la forme d'une séquence brute d'octets. Cela peut sembler familier si l'on a travaillé avec des API graphiques, comme OpenGL ou Vulkan directement.

Pour plus de détails, consultez les pages de documentation QQuick3DGeometry et QQuick3DTextureData.

Voir également CustomMaterial, Effect, QQuick3DGeometry, QQuick3DTextureData, Qt Quick 3D - Exemple d'effet personnalisé, Qt Quick 3D - Exemple de shaders personnalisés, Qt Quick 3D - Exemple de matériaux personnalisés, Qt Quick 3D - Exemple de géométrie personnalisée, et Qt Quick 3D - Exemple de texture procédurale.

© 2026 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.