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: 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" } } }
셰이더 코드
셰이더 코드를 살펴보기 전에 리플렉션을 제어하는 데 사용할 수 있는 몇 가지 파라미터를 확인해 보겠습니다.
depthBias | 이 매개변수는 광선과 오브젝트의 깊이 차이가 특정 임계값 내에 있는지 확인하는 데 사용됩니다. |
rayMaxDistance | 뷰 스페이스에서 광선의 끝점이 얼마나 멀리 떨어져 있는지 제어합니다. |
marchSteps | 계산에 사용되는 단계 수를 제어합니다. 단계 수를 늘리면 각 반복에서 광선이 이동하는 조각의 양이 줄어들고 품질이 향상됩니다. |
refinementSteps | 반사된 광선이 오브젝트에 부딪힌 위치를 찾은 후에는 정확한 위치를 찾기 위해 세분화 프로세스가 수행됩니다. 이 매개변수는 사용해야 하는 단계 수를 제어합니다. marchSteps 값이 작으면 더 나은 결과를 얻을 수 있습니다. |
specular | 0에서 1 사이의 값으로 모델의 반사율을 제어합니다. |
materialColor | 모델에 색상을 지정합니다. 이 색은 반사 색상과 혼합됩니다. |
셰이더는 카메라에서 조각까지의 방향을 가져온 다음 조각의 법선 주위에 반사합니다. 광선의 시작점과 끝점은 뷰 스페이스에서 계산된 다음 이 점들이 스크린 스페이스로 변환됩니다. 스크린 스페이스에서 반사된 광선을 행진하면 품질이 향상된다는 이점이 있습니다. 또한 광선은 뷰 스페이스에서는 먼 거리를 커버할 수 있지만 스크린 스페이스에서는 몇 개의 조각만 커버할 수 있습니다.
시작 조각에서 끝 조각을 가리키는 벡터를 계산하여 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
함수는 행진 후 광선이 화면 외부에 있는지 확인합니다. 이 경우 화면 외부에 대한 정보를 사용할 수 없으므로 반사색이 사용되지 않습니다.
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
을 사용하여 뷰 스페이스에서 오브젝트의 위치를 가져옵니다.
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; }
© 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.