Qt Quick 3D - Beispiel für Spiegelungen im Bildschirmbereich

Demonstriert Reflexionen in Qt Quick 3D.

In diesem Beispiel wird gezeigt, wie Sie mit Screen Space Reflections (SSR) Reflexionen an einem Modell erzeugen. SSR ist ein Nachbearbeitungseffekt, der die Szene durch Hinzufügen von Reflexionen verbessern kann. Die Idee hinter SSR ist, dass die Reflexionen im Screen Space berechnet werden können, nachdem die Objekte gerendert worden sind. Für jedes Fragment wird ein Strahl von der Kamera zu diesem Fragment emittiert und dann an der Normalen des Fragments reflektiert. Danach wird der reflektierte Strahl verfolgt und festgestellt, ob er ein Objekt trifft oder nicht. Wenn ein Objekt getroffen wurde, dann reflektiert das Fragment dieses Objekt. Es gibt Situationen, in denen SSR fehlschlägt. Zum Beispiel, wenn der reflektierte Strahl auf ein Objekt hinter der Kamera trifft. Da die Reflexionen im Bildschirmraum berechnet werden, nachdem die Objekte gerendert wurden, sind keine Informationen über die Farbe der Objekte hinter der Kamera verfügbar. Obwohl SSR einige Nachteile hat, verleiht es der Szene mehr Realismus.

In diesem Beispiel wird SSR mit Hilfe von Custom Materials implementiert, das auf Model verwendet werden kann, wodurch es seine Umgebung reflektiert.

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

Der Rest der Szene besteht aus einigen Objekten, die entweder statisch sind oder über der Oberfläche rotieren, um die Reflexionen zu zeigen.

Node {

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

    Node{

        Model {
            source: "#Sphere"
            position: Qt.vector3d(-400.0, screenSpaceReflectionsView.modelHeight, 0.0)
            materials: DefaultMaterial {
                diffuseColor: "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: DefaultMaterial {
                diffuseColor: "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: DefaultMaterial {
                diffuseColor: "blue"
            }
        }
    }

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

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

Shader-Code

Bevor wir in den Code des Shaders eintauchen, sollten wir einige Parameter überprüfen, die zur Steuerung der Reflexionen verwendet werden können.

depthBiasDieser Parameter wird verwendet, um zu prüfen, ob der Unterschied zwischen der Tiefe des Strahls und des Objekts innerhalb eines bestimmten Schwellenwerts liegt.
rayMaxDistanceSteuert, wie weit der Endpunkt des Strahls im View Space entfernt ist.
marchStepsSteuert, wie viele Schritte für die Berechnung verwendet werden. Wenn Sie die Anzahl der Schritte erhöhen, wird die Anzahl der Fragmente, die der Strahl in jeder Iteration bewegt, verringert und die Qualität erhöht.
refinementStepsNachdem festgestellt wurde, wo der reflektierte Strahl das Objekt getroffen hat, wird ein Verfeinerungsprozess durchgeführt, um den genauen Ort des Treffers zu finden. Dieser Parameter steuert, wie viele Schritte verwendet werden sollen. Er kann zu besseren Ergebnissen führen, wenn die marchSteps klein ist.
specularEin Wert zwischen 0 und 1, um zu steuern, wie viel Reflektivität das Modell hat.
materialColorVerleiht dem Modell eine Farbe. Diese Farbe wird mit der Reflexionsfarbe gemischt.

Der Shader beginnt damit, eine Richtung von der Kamera zum Fragment zu erhalten und reflektiert diese dann um die Normalen des Fragments. Der Start- und der Endpunkt des Strahls werden im View Space berechnet, und diese Punkte werden dann in den Screen Space transformiert. Der Vorteil der Verlegung des reflektierten Strahls in den Bildschirmraum besteht darin, dass die Qualität besser ist. Außerdem kann der Strahl im Ansichtsraum eine große Strecke zurücklegen, im Bildschirmraum jedoch nur wenige Fragmente.

Ein Vektor, der vom Startfragment zum Endfragment zeigt, wird berechnet und durch die marchSteps geteilt.

Danach wird die Funktion rayMarch aufgerufen. Sie verschiebt den Strahl Schritt für Schritt im Screen Space und transformiert ihn dann zurück in den View Space. Außerdem wird das Objekt in diesem Fragment mit Hilfe der DEPTH_TEXTURE der Szene ermittelt. Die Differenz zwischen der Tiefe des Strahls und der Tiefe des Objekts wird berechnet und mit der depthBias verglichen. Wird ein Treffer gefunden, wird die Funktion refinementStep aufgerufen.

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

Der Verfeinerungsschritt ist derselbe wie rayMarch, außer dass versucht wird, die genaue Position zu finden, an der der Treffer auftritt, so dass der Strahl bei jeder Iteration um die halbe Schrittdistanz weiterbewegt wird.

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

Die Sichtbarkeit der Reflexion wird auf den Trefferwert gesetzt, und danach werden einige Sichtbarkeitsprüfungen durchgeführt. Wie bereits erwähnt, schlägt SSR fehl, wenn der reflektierte Strahl auf etwas hinter der Kamera trifft, daher wird die Sichtbarkeit entsprechend der Annäherung des reflektierten Strahls an die Kamera ausgeblendet. Die Sichtbarkeit wird auch in Abhängigkeit davon ausgeblendet, wie weit das getroffene Objekt vom Startpunkt des Strahls entfernt ist.

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

Schließlich wird die Reflexionsfarbe aus der SCREEN_TEXTURE berechnet und mit der Materialfarbe gemischt.

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

Hilfsfunktionen

Es gibt einige Hilfsfunktionen, die im Code des Shaders verwendet werden. Die Funktion correctTextureCoordinates fixiert die Koordinaten der Textur entsprechend der verwendeten Grafik-API. Dies muss im Falle von D3D11 oder Metal durchgeführt werden. Für weitere Informationen siehe CustomMaterial Dokumentation.

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

    return uv;
}

Die Funktion rayFragOutOfBound prüft, ob der Strahl nach dem Marschieren außerhalb des Bildschirms liegt. In diesem Fall wird keine Reflexionsfarbe verwendet, da keine Informationen für etwas außerhalb des Bildschirms verfügbar sind.

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

Die Funktion viewPosFromScreen ermittelt die Position des Objekts im View Space mit Hilfe der Funktion 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;
}

Die Funktion rayViewDepthFromScreen ermittelt die aktuelle Position des Strahls im Ansichtsraum. Dieses Mal wird der Tiefenwert durch lineare Interpolation des Wertes zwischen der Tiefe des Startpunktes und der Tiefe des Endpunktes des Strahls ermittelt.

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

Beispielprojekt @ code.qt.io

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