Sur cette page

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

Démonstration de réflexions dans Qt Quick 3D.

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

Cet exemple montre comment créer des reflets à l'aide de Screen Space Reflections (SSR) sur un modèle. Le SSR est un effet de post-traitement qui permet d'améliorer la scène en y ajoutant des reflets. L'idée derrière SSR est que les réflexions peuvent être calculées dans l'espace écran après que les objets ont été rendus. Pour chaque fragment, un rayon est émis depuis la caméra vers ce fragment, puis il est réfléchi autour de la normale du fragment. Ensuite, nous suivons le rayon réfléchi et déterminons s'il va toucher un objet ou non. Si un objet a été touché, le fragment reflétera cet objet. Dans certaines situations, la SSR échoue. Par exemple, lorsque le rayon réfléchi touche un objet situé derrière la caméra. Étant donné que les réflexions sont calculées 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 ajoute plus de réalisme à la scène.

Cet exemple met en œuvre le SSR à l'aide de Custom Materials qui peut être utilisé sur un site Model, ce qui lui permet 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 tournent au-dessus de la surface pour montrer 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 plonger dans le code du shader, vérifions quelques paramètres qui peuvent être utilisés pour contrôler les réflexions.

depthBiasCe paramètre est utilisé pour vérifier si la différence entre la profondeur du rayon et celle de l'objet est inférieure à un certain seuil.
rayMaxDistanceContrôle la distance du point final du rayon dans l'espace de visualisation.
marchStepsContrôle le nombre d'étapes utilisées pour le calcul. L'augmentation du nombre d'étapes diminue la quantité de fragments que le rayon déplace à chaque itération et augmente la qualité.
refinementStepsAprès avoir trouvé l'endroit où le rayon réfléchi a touché l'objet, un processus d'affinement est effectué pour essayer 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 le site marchSteps est petit.
specularUne valeur entre 0 et 1 pour 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 réflexion.

Le shader commence par obtenir une direction de la caméra vers le fragment, puis la reflète autour de la normale du fragment. Le point de départ et le point d'arrivée du rayon sont calculés dans l'espace de visualisation, puis ces points sont transformés dans l'espace d'affichage. L'avantage de faire défiler le rayon réfléchi dans l'espace-écran est qu'il permet d'obtenir une meilleure qualité. En outre, le rayon peut couvrir une grande distance dans l'espace de visualisation, mais seulement quelques fragments dans l'espace d'affichage.

Un vecteur pointant du fragment de départ au fragment d'arrivée est calculé et divisé par marchSteps.

La fonction rayMarch est ensuite appelée. Elle déplace le rayon à chaque étape dans l'espace-écran, puis le retransforme dans l'espace de visualisation. Elle obtient également l'objet au niveau de ce fragment en utilisant le site 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 fonction depthBias. Si une correspondance est trouvée, 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 la même que celle de rayMarch, sauf qu'elle essaie de trouver la position exacte où le coup se produit, et avance donc 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 fixée à la valeur de l'impact, puis des contrôles de visibilité sont effectués. Comme mentionné précédemment, SSR échouera si le rayon réfléchi touche quelque chose derrière la caméra, donc la visibilité est atténuée en fonction de la direction du rayon réfléchi vers la caméra. La visibilité est également réduite 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 réflexion est calculée à partir de SCREEN_TEXTURE 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

Quelques fonctions d'aide sont utilisées dans le code du shader. La fonction correctTextureCoordinates fixe les coordonnées de la texture en fonction de l'API graphique utilisée. Cela doit être fait dans le cas de D3D11 ou Metal. Pour plus d'informations, voir la documentation de 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 est en dehors de l'écran après la marche. 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 obtient la position de l'objet dans l'espace de visualisation en utilisant 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 obtient la position actuelle du rayon dans l'espace de visualisation. Cette fois, la valeur de la profondeur est obtenue par interpolation linéaire de la valeur entre la profondeur du point de départ et la profondeur 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 @ 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.