Sur cette page

Qt Quick 3D - Exemple de reflets dans l'espace écran

Illustre les reflets en 3D avec la technique « Qt Quick ».

Surface réfléchissante avec des sphères colorées et des objets flottants

Cet exemple montre comment créer des reflets à l'aide de la technique des reflets dans l'espace écran (SSR) sur un modèle. Le SSR est un effet de post-traitement qui permet d'enrichir la scène en y ajoutant des reflets. Le principe du SSR repose sur le fait que les reflets peuvent être calculés dans l'espace écran une fois les objets rendus. Pour chaque fragment, un rayon est émis depuis la caméra vers ce fragment, puis il est réfléchi selon la normale du fragment. Ensuite, on suit le rayon réfléchi et on détermine s’il va heurter un objet ou non. Si un objet est touché, alors le fragment reflétera cet objet. Il existe des situations où le SSR échoue. Par exemple, lorsque le rayon réfléchi touche un objet situé derrière la caméra. Comme les reflets sont calculés dans l’espace écran après le rendu des objets, aucune information sur la couleur des objets situés derrière la caméra n’est disponible. Bien que le SSR présente certains inconvénients, il apporte davantage de réalisme à la scène.

Cet exemple implémente le SSR à l’aide d’ Custom Materials, qui peut être utilisé sur une « Model » (surface réfléchissante), ce qui lui permettra de refléter son environnement.

Model {
    source: "#Rectangle"
    scale: Qt.vector3d(5, 5, 5)
    eulerRotation.x: -90
    eulerRotation.z: 180
    position: Qt.vector3d(0.0, -50.0, 0.0)
    materials: ScreenSpaceReflections {
        depthBias: depthBiasSlider.value
        rayMaxDistance: distanceSlider.value
        marchSteps: marchSlider.value
        refinementSteps: refinementStepsSlider.value
        specular: specularSlider.value
        materialColor: materialColorCheckBox.checked ? "transparent" : "dimgray"
    }
}

Le reste de la scène comporte des objets qui sont soit statiques, soit en rotation au-dessus de la surface afin de mettre en évidence les reflets.

Node {

    Model {
        source: "#Cube"
        eulerRotation.y: 0
        scale: Qt.vector3d(1, 1, 1)
        position: Qt.vector3d(50.0, 40.0, 50.0)
        materials:  PrincipledMaterial {
            baseColorMap: Texture {
                source: "qt_logo_rect.png"
            }
        }
    }

    Node{

        Model {
            source: "#Sphere"
            position: Qt.vector3d(-400.0, screenSpaceReflectionsView.modelHeight, 0.0)
            materials: PrincipledMaterial {
                baseColor: "magenta"
            }
        }
    }

    Node{
        eulerRotation.y: screenSpaceReflectionsView.modelRotation
        position.y: screenSpaceReflectionsView.modelHeight

        Model {
            source: "#Sphere"
            pivot: Qt.vector3d(0, 0.0, 0.0)
            position: Qt.vector3d(200.0, 0.0, 0.0)
            materials: PrincipledMaterial {
                baseColor: "green"
            }
        }
    }

    Node{
        eulerRotation.y: screenSpaceReflectionsView.modelRotation
        position.y: screenSpaceReflectionsView.modelHeight

        Model {
            source: "#Sphere"
            eulerRotation.y: 45
            position: Qt.vector3d(0.0, 0.0, -200.0)
            materials: PrincipledMaterial {
                baseColor: "blue"
            }
        }
    }

    Node{
        eulerRotation.y: screenSpaceReflectionsView.modelRotation
        position.y: screenSpaceReflectionsView.modelHeight

        Model {
            source: "#Sphere"
            position: Qt.vector3d(0.0, 0.0, 200.0)
            materials: PrincipledMaterial {
                baseColor: "red"
            }
        }
    }

Code du shader

Avant de nous plonger dans le code du shader, examinons quelques paramètres permettant de contrôler les reflets.

depthBiasCe paramètre sert à vérifier si la différence entre la profondeur du rayon et celle de l’objet se situe dans une certaine fourchette.
rayMaxDistanceContrôle la distance à laquelle se trouve le point d’arrivée du rayon dans l’espace de vue.
marchStepsDétermine le nombre d’étapes utilisées pour le calcul. Augmenter ce nombre réduit le nombre de fragments parcourus par le rayon à chaque itération et améliore la qualité.
refinementStepsUne fois l'emplacement où le rayon réfléchi a touché l’objet déterminé, un processus de raffinement est effectué pour tenter de trouver l’emplacement exact de l’impact. Ce paramètre contrôle le nombre d’étapes à utiliser. Il peut donner de meilleurs résultats lorsque l’ marchSteps e est faible.
specularUne valeur comprise entre 0 et 1 permettant de contrôler le degré de réflectivité du modèle.
materialColorDonne une couleur au modèle. Cette couleur est mélangée à la couleur de la réflexion.

Le shader commence par déterminer une direction entre la caméra et le fragment, puis fait rebondir le rayon selon la normale du fragment. Le point de départ et le point d’arrivée du rayon sont calculés dans l’espace de vue, puis ces points sont transformés dans l’espace écran. L’avantage de faire progresser le rayon réfléchi dans l’espace écran est d’obtenir une meilleure qualité. De plus, le rayon peut parcourir une grande distance dans l’espace de vue, mais ne toucher que quelques fragments dans l’espace écran.

Un vecteur pointant du fragment de départ vers le fragment d’arrivée est calculé et divisé par l’ marchSteps.

La fonction ` rayMarch ` est ensuite appelée. Elle fait progresser le rayon à chaque pas dans l’espace écran, puis le retransforme dans l’espace de vue. Elle récupère également l’objet au niveau de ce fragment à l’aide de la ` DEPTH_TEXTURE ` de la scène. La différence entre les profondeurs du rayon et de l’objet est calculée et comparée à la ` depthBias`. Si un impact est détecté, la fonction ` refinementStep ` est appelée.

void rayMarch(vec2 rayStepVector, vec2 size)
{
    for(int i = 0; i < marchSteps; i++)
    {
        rayData.rayFragCurr += rayStepVector;
        rayData.rayCoveredPart = length(rayData.rayFragCurr - rayData.rayFragStart) / length(rayData.rayFragEnd - rayData.rayFragStart);
        rayData.rayCoveredPart = clamp(rayData.rayCoveredPart, 0.0, 1.0);
        float rayDepth = rayViewDepthFromScreen(size);
        rayData.objHitViewPos = viewPosFromScreen(rayData.rayFragCurr, size);
        float deltaDepth = rayDepth - rayData.objHitViewPos.z;

        if(deltaDepth > 0 && deltaDepth < depthBias)
        {
            rayData.hit = 1;
            refinementStep(rayStepVector, size);
            return;
        }
    }
}

L’étape de raffinement est identique à celle décrite dans rayMarch, à ceci près qu’elle cherche à déterminer la position exacte du point de collision ; pour cela, elle fait avancer le rayon de la moitié de la distance de l’étape à chaque itération.

void refinementStep(vec2 rayStepVector, vec2 size)
{
    for(int i = 0; i < refinementSteps; i++)
    {
        rayData.rayCoveredPart = length(rayData.rayFragCurr - rayData.rayFragStart) / length(rayData.rayFragEnd - rayData.rayFragStart);
        rayData.rayCoveredPart = clamp(rayData.rayCoveredPart, 0.0, 1.0);
        float rayDepth = rayViewDepthFromScreen(size);
        rayData.objHitViewPos = viewPosFromScreen(rayData.rayFragCurr, size);
        float deltaDepth = rayDepth - rayData.objHitViewPos.z;

        rayStepVector *= 0.5;
        if(deltaDepth > 0 && deltaDepth < depthBias)
            rayData.rayFragCurr -= rayStepVector;
        else
            rayData.rayFragCurr += rayStepVector;
    }
}

La visibilité de la réflexion est définie sur la valeur de l’impact, puis plusieurs vérifications de visibilité sont effectuées. Comme mentionné précédemment, le SSR échouera si le rayon réfléchi touche un objet situé derrière la caméra ; la visibilité est donc atténuée en fonction de la direction dans laquelle le rayon réfléchi se dirige vers la caméra. La visibilité est également atténuée en fonction de la distance entre l’objet touché et le point de départ du rayon.

float visibility = rayData.hit;
/* Check if the ray hit an object behind the camera. This means information about the object can not be obtained from SCREEN_TEXTURE.
   Start fading the visibility according to how much the reflected ray is moving toward the opposite direction of the camera */
visibility *= (1 - max(dot(-normalize(fragViewPos), reflected), 0));

/* Fade out visibility according how far is the hit object from the fragment */
visibility *= (1 - clamp(length(rayData.objHitViewPos - rayData.rayViewStart) / rayMaxDistance, 0, 1));
visibility = clamp(visibility, 0, 1);

Enfin, la couleur de la réflexion est calculée à partir de l’ SCREEN_TEXTURE e et mélangée à la couleur du matériau.

vec2 uv = rayData.rayFragCurr / size;
uv = correctTextureCoordinates(uv);
vec3 reflectionColor = texture(SCREEN_TEXTURE, uv).rgb;
reflectionColor *= specular;

vec3 mixedColor = mix(materialColor.rgb, reflectionColor, visibility);
BASE_COLOR = vec4(mixedColor, materialColor.a);

Fonctions d’aide

Le code du shader utilise plusieurs fonctions auxiliaires. La fonction ` correctTextureCoordinates ` corrige les coordonnées de la texture en fonction de l’API graphique utilisée. Cette opération est nécessaire dans le cas de D3D11 ou de Metal. Pour plus d’informations, consultez la documentation sur l’ CustomMaterial.

vec2 correctTextureCoordinates(vec2 uv)
{
    if(FRAMEBUFFER_Y_UP < 0 && NDC_Y_UP == 1)
        uv.y = 1 - uv.y;

    return uv;
}

La fonction ` rayFragOutOfBound ` vérifie si le rayon se trouve en dehors de l’écran après la marche des rayons. Dans ce cas, aucune couleur de réflexion n’est utilisée, car aucune information n’est disponible pour ce qui se trouve en dehors de l’écran.

bool rayFragOutOfBound(vec2 rayFrag, vec2 size)
{
    if(rayFrag.x > size.x || rayFrag.y > size.y)
        return true;

    if(rayFrag.x < 0 || rayFrag.y < 0)
        return true;

    return false;
}

La fonction ` viewPosFromScreen ` récupère la position de l’objet dans l’espace de vue à l’aide de la fonction ` DEPTH_TEXTURE`.

vec3 viewPosFromScreen(vec2 fragPos, vec2 size)
{
    vec2 uv = fragPos / size;
    vec2 texuv = correctTextureCoordinates(uv);

    float depth = textureLod(DEPTH_TEXTURE, texuv, 0).r;
    if(NEAR_CLIP_VALUE  < 0.0)
        depth = 2 * depth - 1.0;

    vec3 ndc = vec3(2 * uv - 1, depth);
    vec4 viewPos = INVERSE_PROJECTION_MATRIX * vec4(ndc, 1.0);
    viewPos /= viewPos.w;
    return viewPos.xyz;
}

La fonction ` rayViewDepthFromScreen ` récupère la position actuelle du rayon dans l'espace de vue. Cette fois-ci, la valeur de profondeur est obtenue par interpolation linéaire entre la profondeur du point de départ et celle du point d'arrivée du rayon.

float rayViewDepthFromScreen(vec2 size)
{
    vec2 uv = rayData.rayFragCurr / size;
    float depth = mix(rayData.rayFragStartDepth, rayData.rayFragEndDepth, rayData.rayCoveredPart);
    vec3 ndc = vec3(2 * uv - 1, depth);
    vec4 viewPos = INVERSE_PROJECTION_MATRIX * vec4(ndc, 1.0);
    viewPos /= viewPos.w;
    return viewPos.z;
}

Exemple de projet sur code.qt.io

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