En esta página

Qt Quick 3D - Ejemplo de reflejos en el espacio de la pantalla

Demuestra reflexiones en Qt Quick 3D.

Superficie reflectante con esferas de colores y objetos flotantes

Este ejemplo demuestra cómo crear reflejos utilizando Reflejos de Espacio en Pantalla (SSR) en un modelo. SSR es un efecto de post-procesado que puede mejorar la escena añadiéndole reflejos. La idea detrás de SSR es que las reflexiones se pueden calcular en Screen Space después de que los objetos han sido renderizados. Para cada fragmento, se emite un rayo desde la cámara a este fragmento y luego se refleja alrededor de la normal del fragmento. Después, seguimos el rayo reflejado y determinamos si golpeará un objeto o no. Si un objeto fue golpeado, entonces el fragmento reflejará este objeto. Hay situaciones en las que el SSR fallará. Por ejemplo, cuando el rayo reflejado golpea un objeto detrás de la cámara. Como las reflexiones se calculan en el espacio de la pantalla después de que los objetos hayan sido renderizados, no hay información disponible sobre el color de los objetos detrás de la cámara. Aunque SSR tiene algunos inconvenientes, añade más realismo a la escena.

Este ejemplo implementa SSR utilizando Custom Materials que se puede utilizar en un Model, lo que hará que refleje su entorno.

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

El resto de la escena tiene algunos objetos que son estáticos o rotan sobre la superficie para mostrar los reflejos.

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

Código del shader

Antes de entrar en el código del shader, vamos a comprobar algunos parámetros que se pueden utilizar para controlar los reflejos.

depthBiasEste parámetro se utiliza para comprobar si la diferencia entre la profundidad del rayo y el objeto está dentro de un cierto umbral.
rayMaxDistanceControla a qué distancia está el punto final del rayo en el Espacio de Visión.
marchStepsControla cuántos pasos se utilizan para el cálculo. Aumentar el número de pasos disminuye la cantidad de fragmentos que el rayo se mueve en cada iteración y aumenta la calidad.
refinementStepsDespués de encontrar donde el rayo reflejado golpea el objeto, se realiza un proceso de refinamiento para intentar encontrar la localización exacta del golpe. Este parámetro controla cuántos pasos deben utilizarse. Puede dar mejores resultados cuando marchSteps es pequeño.
specularUn valor entre 0 y 1 para controlar cuánta reflectividad tiene el modelo.
materialColorDa color al modelo. Este color se mezcla con el color de reflexión.

El sombreador comienza obteniendo una dirección desde la cámara al fragmento, y luego la refleja alrededor de la normal del fragmento. El punto inicial y el punto final del rayo se calculan en View Space, y luego estos puntos se transforman en Screen Space. La ventaja de marchar el rayo reflejado en el Espacio de Pantalla es que se obtiene una mejor calidad. Además, el rayo puede cubrir una gran distancia en el espacio de visión, pero sólo unos pocos fragmentos en el espacio de la pantalla.

Se calcula un vector que apunta desde el fragmento inicial hasta el fragmento final y se divide por marchSteps.

A continuación se llama a la función rayMarch. Hace marchar el rayo a cada paso en el Espacio Pantalla, y luego lo transforma de vuelta al Espacio Vista. También obtiene el objeto en este fragmento utilizando el DEPTH_TEXTURE de la escena. La diferencia entre la profundidad del rayo y la del objeto se calcula y se compara con depthBias. Si se encuentra una coincidencia, se llama a la función refinementStep.

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

El paso de refinamiento es el mismo que rayMarch, excepto que intenta encontrar la posición exacta donde se produce el impacto, por lo que avanza el rayo la mitad de la distancia del paso en cada iteración.

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 visibilidad del reflejo se establece en el valor del impacto, y después se realizan algunas comprobaciones de visibilidad. Como se mencionó anteriormente, SSR fallará si el rayo reflejado golpea algo detrás de la cámara, por lo que la visibilidad se desvanece de acuerdo a cuánto se dirige el rayo reflejado hacia la cámara. La visibilidad también se atenúa en función de la distancia a la que el objeto golpeado se encuentra del punto de inicio del rayo.

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

Por último, el color del reflejo se calcula a partir de SCREEN_TEXTURE y se mezcla con el color del material.

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

Funciones auxiliares

Hay algunas funciones de ayuda utilizadas en el código del shader. La función correctTextureCoordinates fija la coordenada de la textura de acuerdo con la API gráfica utilizada. Esto debe hacerse en el caso de D3D11 o Metal. Para más información, consulte la documentación de CustomMaterial.

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

    return uv;
}

La función rayFragOutOfBound comprueba si el rayo está fuera de la pantalla después de marchar. En este caso, no se utiliza ningún color de reflexión porque no hay información disponible para nada fuera de la pantalla.

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 función viewPosFromScreen obtiene la posición del objeto en el Espacio de Vista utilizando la función 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 función rayViewDepthFromScreen obtiene la posición actual del rayo en el Espacio de Visión. Esta vez el valor de la profundidad se obtiene interpolando linealmente el valor entre la profundidad del punto inicial y la profundidad del punto final del rayo.

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

Proyecto de ejemplo @ 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.