이 페이지에서

Qt Quick 3D - 스크린 스페이스 리플렉션 예제

Qt Quick 3D 에서의 반사 효과를 보여줍니다.

색색의 구슬과 떠 있는 물체들이 있는 반사면

이 예제는 모델에 스크린 스페이스 리플렉션 (SSR)을 사용하여 반사 효과를 구현하는 방법을 보여줍니다. SSR은 장면에 반사 효과를 추가하여 장면을 더욱 생동감 있게 만들어 주는 후처리 효과입니다. SSR의 핵심 원리는 오브젝트가 렌더링된 후 스크린 스페이스에서 반사광을 계산할 수 있다는 점입니다. 각 프래그먼트에 대해 카메라에서 해당 프래그먼트로 광선이 발사된 후, 프래그먼트의 법선 방향을 중심으로 반사됩니다. 그 후, 반사된 광선을 따라가며 해당 광선이 오브젝트에 부딪힐지 여부를 판단합니다. 오브젝트에 부딪힌 경우, 해당 프래그먼트는 이 오브젝트를 반사합니다. SSR이 제대로 작동하지 않는 상황도 있습니다. 예를 들어, 반사된 광선이 카메라 뒤쪽의 오브젝트에 부딪히는 경우입니다. 반사는 오브젝트가 렌더링된 후 스크린 스페이스에서 계산되므로, 카메라 뒤쪽 오브젝트의 색상에 대한 정보는 사용할 수 없습니다. SSR에는 몇 가지 단점이 있지만, 장면에 더욱 사실감을 더해줍니다.

이 예제에서는 Model 에서 사용할 수 있는 원형 반사체( Custom Materials )를 활용해 SSR을 구현하며, 이를 통해 주변 환경이 반사되도록 합니다.

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

장면의 나머지 부분에는 반사를 보여주기 위해 정적 상태이거나 표면 위에서 회전하는 오브젝트들이 있습니다.

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

셰이더 코드

셰이더 코드를 자세히 살펴보기 전에, 반사를 제어하는 데 사용할 수 있는 몇 가지 매개변수를 확인해 봅시다.

depthBias이 매개변수는 레이의 깊이와 오브젝트 사이의 차이가 특정 임계값 이내인지 확인하는 데 사용됩니다.
rayMaxDistance뷰 스페이스에서 레이의 종착점이 얼마나 멀리 있는지 제어합니다.
marchSteps계산에 사용되는 단계 수를 제어합니다. 단계 수를 늘리면 각 반복에서 레이가 이동하는 프래그먼트의 양이 줄어들고 품질이 향상됩니다.
refinementSteps반사된 레이가 오브젝트에 부딪힌 위치를 찾은 후, 정확한 충돌 위치를 파악하기 위해 정밀화 과정이 수행됩니다. 이 매개변수는 사용될 단계 수를 제어합니다. 정밀화 단계( marchSteps )가 작을 때 더 나은 결과를 얻을 수 있습니다.
specular0과 1 사이의 값으로 모델의 반사율을 조절합니다.
materialColor모델에 색상을 부여합니다. 이 색상은 반사 색상과 혼합됩니다.

셰이더는 먼저 카메라에서 프래그먼트까지의 방향을 구한 다음, 이를 프래그먼트의 법선 방향으로 반사시킵니다. 광선의 시작점과 끝점은 뷰 스페이스(View Space)에서 계산된 후, 스크린 스페이스(Screen Space)로 변환됩니다. 반사된 광선을 스크린 스페이스에서 추적하는 이점은 더 나은 품질을 얻을 수 있다는 것입니다. 또한, 광선은 뷰 스페이스에서는 먼 거리를 이동할 수 있지만 스크린 스페이스에서는 소수의 프래그먼트만 통과할 수도 있습니다.

시작 프래그먼트에서 끝 프래그먼트로 향하는 벡터가 계산되어 ` marchSteps`로 나뉩니다.

그 후 ` rayMarch ` 함수가 호출됩니다. 이 함수는 스크린 공간에서 매 단계마다 레이를 진행시킨 다음, 다시 뷰 공간으로 변환합니다. 또한 장면의 ` DEPTH_TEXTURE `를 사용하여 해당 프래그먼트의 오브젝트를 가져옵니다. 레이와 오브젝트의 깊이 차이를 계산하여 ` depthBias`와 비교합니다. 충돌이 발견되면 ` 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;
        }
    }
}

정밀화 단계는 rayMarch 와 동일하지만, 충돌이 발생한 정확한 위치를 찾으려고 시도한다는 점이 다르며, 따라서 반복할 때마다 레이를 단계 거리의 절반만큼 전진시킵니다.

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

반사의 가시성은 히트 값으로 설정되며, 그 후 몇 가지 가시성 검사가 수행됩니다. 앞서 언급한 바와 같이, 반사된 광선이 카메라 뒤의 물체에 부딪히면 SSR이 실패하므로, 반사된 광선이 카메라를 향하는 정도에 따라 가시성이 점차 사라지게 됩니다. 또한, 충돌한 물체가 광선의 시작점에서 얼마나 멀리 떨어져 있는지에 따라 가시성이 점차 사라집니다.

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

마지막으로, 반사 색상은 반사 계수( SCREEN_TEXTURE )를 기반으로 계산되어 머티리얼 색상과 혼합됩니다.

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

보조 함수

셰이더 코드에는 몇 가지 보조 함수가 사용됩니다. ` correctTextureCoordinates ` 함수는 사용 중인 그래픽스 API에 따라 텍스처 좌표를 보정합니다. 이는 D3D11이나 Metal의 경우 반드시 수행되어야 합니다. 자세한 내용은 CustomMaterial 문서를 참조하십시오.

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

    return uv;
}

rayFragOutOfBound 함수는 레이가 행진(marching) 후 화면 외부에 있는지 확인합니다. 이 경우, 화면 외부의 정보는 얻을 수 없으므로 반사 색상이 사용되지 않습니다.

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

viewPosFromScreen 함수는 DEPTH_TEXTURE 를 사용하여 뷰 공간(View Space) 내 객체의 위치를 구합니다.

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

rayViewDepthFromScreen 함수는 뷰 공간에서 광선의 현재 위치를 가져옵니다. 이때 깊이 값은 광선의 시작점 깊이와 끝점 깊이 사이의 값을 선형 보간하여 구합니다.

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

예제 프로젝트 @ 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.