Qt 3D 렌더링 프레임그래프
Qt 3D 렌더링 측면에서는 렌더링 알고리즘이 완전히 데이터 기반으로 작동할 수 있습니다. 이를 제어하는 데이터 구조를 프레임그래프라고 합니다. Qt 3D ECS(엔티티 컴포넌트 시스템)에서 엔티티와 컴포넌트의 트리에서 장면을 구축하여 소위 장면 그래프를 정의할 수 있는 것과 마찬가지로 프레임그래프도 트리 구조이지만 다른 용도로 사용됩니다. 즉, 장면이 렌더링되는 방식을 제어하는 것입니다.
단일 프레임을 렌더링하는 동안 3D 렌더러는 상태를 여러 번 변경할 수 있습니다. 이러한 상태 변경의 수와 특성은 씬 내에 어떤 머티리얼(셰이더, 메시 지오메트리, 텍스처 및 균일 변수)이 있는지뿐만 아니라 어떤 상위 레벨 렌더링 방식을 사용하는지에 따라 달라집니다.
예를 들어, 기존의 단순 포워드 렌더링 방식을 사용하는 것과 디퍼드 렌더링 방식을 사용하는 것은 매우 다릅니다. 리플렉션, 그림자, 다중 뷰포트, 초기 Z-필 패스와 같은 다른 기능은 모두 렌더러가 한 프레임 동안 설정해야 하는 상태와 이러한 상태 변경이 필요한 시기를 변경합니다.
비교를 위해 Qt Quick 2 장면을 그리는 Qt Quick 2 시나리오 렌더러는 프리미티브 일괄 처리, 불투명 항목 렌더링 후 투명 항목 렌더링 등의 작업을 수행하도록 C++에서 하드 와이어링되어 있습니다. Qt Quick 2의 경우 모든 요구 사항을 충족하므로 문제가 없습니다. 위에 나열된 몇 가지 예에서 볼 수 있듯이, 이러한 하드 와이어 렌더러는 다양한 렌더링 방법을 사용할 수 있는 일반적인 3D 장면에 충분히 유연하지 않을 가능성이 높습니다. 또는 렌더러를 이러한 모든 경우에 적용할 수 있을 만큼 유연하게 만들 수 있다고 해도 너무 일반적이어서 성능이 저하될 가능성이 높습니다. 설상가상으로 더 많은 렌더링 방법이 계속 연구되고 있습니다. 따라서 사용과 유지 관리가 간편하면서도 유연하고 확장 가능한 접근 방식이 필요했습니다. 프레임그래프를 시작하세요!
프레임그래프의 각 노드는 렌더러가 장면을 렌더링하는 데 사용할 구성의 일부를 정의합니다. 프레임그래프 트리에서 노드의 위치에 따라 해당 노드에 루팅된 하위 트리가 렌더링 파이프라인에서 활성 구성이 되는 시기와 위치가 결정됩니다. 나중에 살펴보겠지만 렌더러는 프레임의 각 지점에서 렌더링 알고리즘에 필요한 상태를 구축하기 위해 이 트리를 탐색합니다.
물론 화면에 단순한 큐브를 렌더링하려는 경우라면 이 작업이 과하다고 생각할 수 있습니다. 하지만 조금 더 복잡한 장면을 렌더링하고 싶을 때는 이 기능이 유용합니다. 일반적인 경우에는 Qt 3D 에서 바로 사용할 수 있는 몇 가지 예제 프레임그래프를 제공합니다.
몇 가지 예시와 결과 프레임그래프를 제시하여 프레임그래프 개념의 유연성을 보여드리겠습니다.
엔티티와 컴포넌트로 구성된 시나리오그래프와 달리 프레임그래프는 모두 Qt3DRender::QFrameGraphNode 의 서브클래스인 중첩된 노드로만 구성된다는 점에 유의하세요. 프레임그래프 노드는 가상 세계에서 시뮬레이션된 객체가 아니라 정보를 지원하는 역할을 하기 때문입니다.
곧 첫 번째 간단한 프레임그래프를 구성하는 방법을 살펴보겠지만 그 전에 사용할 수 있는 프레임그래프 노드를 소개하겠습니다. 또한 시나리오 그래프 트리와 마찬가지로 QML과 C++ API는 1대 1로 매칭되므로 가장 마음에 드는 것을 선택할 수 있습니다. 이 글에서는 가독성과 간결성을 위해 QML API를 선택했습니다.
프레임그래프의 장점은 이러한 간단한 노드 유형을 결합하면 털이 많은 저수준 C/C++ 렌더링 코드를 전혀 건드리지 않고도 특정 요구 사항에 맞게 렌더러를 구성할 수 있다는 것입니다.
프레임그래프 규칙
올바르게 작동하는 프레임그래프 트리를 구성하려면 트리를 탐색하는 방법과 Qt 3D 렌더러에 공급하는 방법에 대한 몇 가지 규칙을 알고 있어야 합니다.
프레임그래프 설정하기
프레임그래프 트리는 그 자체가 Qt 3D 씬의 루트 엔티티의 컴포넌트인 QRenderSettings 컴포넌트의 activeFrameGraph 프로퍼티에 할당되어야 합니다. 이것이 렌더러의 활성 프레임그래프가 되는 것입니다. 물론 이것은 QML 속성 바인딩이므로 런타임에 활성 프레임그래프(또는 그 일부)를 즉석에서 변경할 수 있습니다. 예를 들어 실내 및 실외 장면에 서로 다른 렌더링 방식을 사용하거나 특수 효과를 활성화 또는 비활성화하려는 경우입니다.
Entity { id: sceneRoot components: RenderSettings { activeFrameGraph: ... // FrameGraph tree } }
참고: 활성 프레임그래프는 QML에서 프레임그래프 컴포넌트의 기본 속성입니다.
Entity { id: sceneRoot components: RenderSettings { ... // FrameGraph tree } }
프레임그래프 사용 방법
- Qt 3D 렌더러는 프레임그래프 트리의 깊이 우선 순회를 수행합니다. 깊이 우선 순회이므로 노드를 정의하는 순서가 중요합니다.
- 렌더러가 프레임그래프의 리프 노드에 도달하면 리프 노드에서 루트 노드까지의 경로에 지정된 모든 상태를 함께 수집합니다. 이는 프레임의 한 부분을 렌더링하는 데 사용되는 상태를 정의합니다. Qt 3D 의 내부에 관심이 있는 경우 이 상태 컬렉션을 렌더뷰라고 합니다.
- RenderView에 포함된 구성이 주어지면 렌더러는 렌더링할 장면 그래프의 모든 엔티티를 함께 수집하고, 이로부터 RenderCommand 세트를 빌드하여 RenderView와 연결합니다.
- RenderView와 RenderCommand 세트의 조합은 OpenGL에 제출하기 위해 전달됩니다.
- 프레임그래프의 각 리프 노드에 대해 이 과정이 반복되면 프레임이 완성되고 렌더러는 QOpenGLContext::swapBuffers()를 호출하여 프레임을 표시합니다.
프레임그래프의 핵심은 Qt 3D 렌더러를 구성하는 데이터 기반 메서드입니다. 데이터 기반 방식이기 때문에 런타임에 구성을 변경할 수 있고, C++가 아닌 개발자나 디자이너도 프레임의 구조를 변경할 수 있으며, 수천 줄의 보일러 플레이트 코드를 작성하지 않고도 새로운 렌더링 방식을 시도해 볼 수 있습니다.
프레임그래프 예시
이제 프레임그래프 트리를 작성할 때 지켜야 할 규칙을 알았으니 몇 가지 예시를 통해 이를 세분화해 보겠습니다.
간단한 포워드 렌더러
포워드 렌더링은 기존 방식대로 OpenGL을 사용하여 백버퍼에 한 번에 하나의 오브젝트를 직접 렌더링하면서 각 오브젝트에 음영을 입히는 방식입니다. 이는 중간 G 버퍼에 렌더링하는 디퍼드 렌더링과 반대되는 방식입니다. 다음은 포워드 렌더링에 사용할 수 있는 간단한 프레임그래프입니다:
Viewport { normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) property alias camera: cameraSelector.camera ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer CameraSelector { id: cameraSelector } } }
보시다시피 이 트리에는 하나의 리프가 있으며 다음 다이어그램과 같이 총 3개의 노드로 구성되어 있습니다.
위에 정의된 규칙을 사용하면 이 프레임그래프 트리는 다음과 같은 구성의 단일 RenderView를 생성합니다:
- 리프 노드 -> 렌더뷰
- 전체 화면을 채우는 뷰포트(중첩된 뷰포트를 쉽게 지원하기 위해 정규화된 좌표를 사용함)
- 컬러 및 뎁스 버퍼가 지워지도록 설정됨
- 노출된 카메라 프로퍼티에 지정된 카메라
여러 개의 다른 프레임그래프 트리가 동일한 렌더링 결과를 생성할 수 있습니다. 리프에서 루트까지 수집된 상태가 동일하다면 결과도 동일합니다. 가장 오래 일정하게 유지되는 상태를 프레임그래프의 루트 가까이에 배치하는 것이 가장 좋으며, 이렇게 하면 리프 노드 수가 줄어들어 전체적으로 렌더뷰 수가 줄어들게 됩니다.
Viewport { normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) property alias camera: cameraSelector.camera CameraSelector { id: cameraSelector ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer } } }
CameraSelector { Viewport { normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer } } }
멀티 뷰포트 프레임그래프
이제 4개의 가상 카메라 시점의 장면 그래프를 창의 4사분면으로 렌더링하는 조금 더 복잡한 예제로 넘어가 보겠습니다. 이는 3D CAD 또는 모델링 툴의 일반적인 구성이며, 자동차 레이싱 게임이나 CCTV 카메라 디스플레이에서 백미러를 렌더링하는 데 도움이 되도록 조정할 수도 있습니다.
Viewport { id: mainViewport normalizedRect: Qt.rect(0, 0, 1, 1) property alias Camera: cameraSelectorTopLeftViewport.camera property alias Camera: cameraSelectorTopRightViewport.camera property alias Camera: cameraSelectorBottomLeftViewport.camera property alias Camera: cameraSelectorBottomRightViewport.camera ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer } Viewport { id: topLeftViewport normalizedRect: Qt.rect(0, 0, 0.5, 0.5) CameraSelector { id: cameraSelectorTopLeftViewport } } Viewport { id: topRightViewport normalizedRect: Qt.rect(0.5, 0, 0.5, 0.5) CameraSelector { id: cameraSelectorTopRightViewport } } Viewport { id: bottomLeftViewport normalizedRect: Qt.rect(0, 0.5, 0.5, 0.5) CameraSelector { id: cameraSelectorBottomLeftViewport } } Viewport { id: bottomRightViewport normalizedRect: Qt.rect(0.5, 0.5, 0.5, 0.5) CameraSelector { id: cameraSelectorBottomRightViewport } } }
이 트리는 잎이 5개로 조금 더 복잡합니다. 이전과 동일한 규칙에 따라 프레임그래프에서 5개의 렌더뷰 객체를 구성합니다. 다음 다이어그램은 처음 두 개의 렌더뷰에 대한 구성을 보여줍니다. 나머지 렌더뷰는 다른 하위 트리가 있을 뿐 두 번째 다이어그램과 매우 유사합니다.
전체적으로 생성된 렌더뷰는 다음과 같습니다:
- 렌더뷰 (1)
- 전체 화면 뷰포트 정의
- 색상 및 뎁스 버퍼가 지워지도록 설정됨
- 렌더뷰 (2)
- 전체 화면 뷰포트 정의됨
- 하위 뷰포트가 정의됨 (렌더링 뷰포트가 부모를 기준으로 스케일 조정됨)
- CameraSelector 지정됨
- RenderView (3)
- 전체 화면 뷰포트 정의
- 하위 뷰포트가 정의됨 (렌더링 뷰포트가 부모를 기준으로 스케일 조정됨)
- CameraSelector 지정됨
- RenderView (4)
- 전체 화면 뷰포트 정의
- 하위 뷰포트가 정의됨 (렌더링 뷰포트가 부모를 기준으로 스케일 조정됨)
- CameraSelector 지정됨
- RenderView (5)
- 전체 화면 뷰포트 정의됨
- 하위 뷰포트가 정의됨 (렌더링 뷰포트가 부모를 기준으로 스케일 조정됨)
- CameraSelector 지정됨
그러나 이 경우 순서가 중요합니다. ClearBuffers 노드를 첫 번째가 아닌 마지막에 배치하면 모든 렌더링이 완료된 직후에 모든 것이 지워지기 때문에 검은색 화면이 나타납니다. 비슷한 이유로 프레임그래프의 루트로 사용할 수 없는데, 이는 각 뷰포트의 전체 화면을 지우는 호출이 발생하기 때문입니다.
프레임그래프의 선언 순서가 중요하지만, Qt 3D 는 각 렌더뷰가 다른 렌더뷰와 독립적이기 때문에 렌더뷰의 상태가 유효한 동안 제출할 렌더커맨드 세트를 생성할 목적으로 각 렌더뷰를 병렬로 처리할 수 있습니다.
Qt 3D 는 사용 가능한 코어 수에 따라 자연스럽게 확장되는 작업 기반 병렬 처리 방식을 사용합니다. 이는 이전 예제의 다음 다이어그램에 나와 있습니다.
렌더뷰에 대한 렌더커맨드는 여러 코어에서 병렬로 생성될 수 있으며, 전용 OpenGL 제출 스레드에서 올바른 순서로 렌더뷰를 제출하기만 하면 결과 장면이 올바르게 렌더링됩니다.
디퍼드 렌더러
렌더링과 관련하여 디퍼드 렌더링은 포워드 렌더링과 비교했을 때 렌더러 구성 측면에서 다른 존재입니다. 각 메시를 그리고 셰이더 효과를 적용하여 음영 처리하는 대신 디퍼드 렌더링은 두 가지 렌더 패스 방식을 채택합니다.
먼저 씬의 모든 메시가 동일한 셰이더를 사용하여 그려지며, 일반적으로 각 조각에 대해 최소 4개의 값을 출력합니다:
- 월드 노멀 벡터
- 색상(또는 기타 머티리얼 속성)
- 깊이
- 월드 위치 벡터
이러한 각 값은 텍스처에 저장됩니다. 노멀, 컬러, 뎁스 및 위치 텍스처는 G-버퍼라고 하는 것을 형성합니다. 첫 번째 패스에서는 화면에 아무것도 그려지지 않고 나중에 사용할 수 있도록 G-버퍼에 그려집니다.
모든 메시가 그려지면 G-버퍼는 현재 카메라에서 볼 수 있는 모든 메시로 채워집니다. 그런 다음 두 번째 렌더 패스를 사용하여 G-버퍼 텍스처에서 노멀, 색상 및 위치 값을 읽고 전체 화면 쿼드에 색상을 출력하여 최종 색상 음영으로 씬을 백 버퍼에 렌더링합니다.
이 기법의 장점은 복잡한 효과에 필요한 무거운 컴퓨팅 성능을 두 번째 패스에서 실제로 카메라에 보이는 요소에만 사용한다는 것입니다. 첫 번째 패스에서는 모든 메시가 단순한 셰이더로 그려지기 때문에 처리 능력이 많이 필요하지 않습니다. 따라서 디퍼드 렌더링은 씬의 오브젝트 수에서 셰이딩과 라이팅을 분리하고 대신 화면의 해상도(및 G-버퍼)에 연결합니다. GPU 메모리를 추가로 사용하는 대신 많은 수의 동적 조명을 사용할 수 있기 때문에 많은 게임에서 사용되는 기법입니다.
Viewport { id: root normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0) property GBuffer gBuffer property alias camera: sceneCameraSelector.camera property alias sceneLayer: sceneLayerFilter.layers property alias screenQuadLayer: screenQuadLayerFilter.layers RenderSurfaceSelector { CameraSelector { id: sceneCameraSelector // Fill G-Buffer LayerFilter { id: sceneLayerFilter RenderTargetSelector { id: gBufferTargetSelector target: gBuffer ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer RenderPassFilter { id: geometryPass matchAny: FilterKey { name: "pass" value: "geometry" } } } } } TechniqueFilter { parameters: [ Parameter { name: "color"; value: gBuffer.color }, Parameter { name: "position"; value: gBuffer.position }, Parameter { name: "normal"; value: gBuffer.normal }, Parameter { name: "depth"; value: gBuffer.depth } ] RenderStateSet { // Render FullScreen Quad renderStates: [ BlendEquation { blendFunction: BlendEquation.Add }, BlendEquationArguments { sourceRgb: BlendEquationArguments.SourceAlpha destinationRgb: BlendEquationArguments.DestinationColor } ] LayerFilter { id: screenQuadLayerFilter ClearBuffers { buffers: ClearBuffers.ColorDepthBuffer RenderPassFilter { matchAny: FilterKey { name: "pass" value: "final" } parameters: Parameter { name: "winSize" value: Qt.size(1024, 768) } } } } } } } } }
(위 코드는 qt3d/tests/manual/deferred-renderer-qml에서 가져온 것입니다.)
그래픽적으로 결과 프레임그래프는 다음과 같습니다:
결과 렌더뷰는 다음과 같습니다:
- 렌더뷰 (1)
- 사용할 카메라 지정
- 전체 화면을 채우는 뷰포트를 정의합니다.
- 레이어 컴포넌트 장면 레이어에 대한 모든 엔티티를 선택합니다.
gBuffer
을 활성 렌더링 대상으로 설정합니다.- 현재 바인딩된 렌더 대상(
gBuffer
)의 색상과 깊이를 지웁니다. - 씬에서 머티리얼과 테크닉이 일치하는 엔티티만 선택합니다. RenderPassFilter
- 렌더뷰 (2)
- 전체 화면을 채우는 뷰포트 정의
- 레이어 컴포넌트 스크린쿼드 레이어에 대한 모든 엔티티를 선택합니다.
- 현재 바인딩된 프레임버퍼(화면)의 색상 및 깊이 버퍼를 지웁니다.
- 씬에서 머티리얼과 테크닉이 어노테이션과 일치하는 엔티티만 선택합니다. RenderPassFilter
프레임그래프의 다른 이점
프레임그래프 트리는 전적으로 데이터 기반이며 런타임에 동적으로 수정할 수 있습니다:
- 플랫폼과 하드웨어에 따라 서로 다른 프레임그래프 트리를 사용하고 런타임에 가장 적합한 트리를 선택할 수 있습니다.
- 씬에서 시각적 디버깅을 쉽게 추가하고 활성화할 수 있습니다.
- 씬의 특정 영역에 대해 렌더링해야 하는 대상의 특성에 따라 다른 프레임그래프 트리 사용
- Qt 3D 의 내부를 수정하지 않고도 새로운 렌더링 기법 구현 가능
결론
지금까지 프레임그래프와 이를 구성하는 노드 유형에 대해 소개했습니다. 그런 다음 프레임그래프 작성 규칙과 Qt 3D 엔진이 프레임그래프를 백그라운드에서 사용하는 방법을 설명하기 위해 몇 가지 예제를 살펴보았습니다. 이제 프레임그래프에 대한 개요와 프레임그래프의 사용 방법(포워드 렌더러에 초기 z-필 패스를 추가하는 것 등)을 꽤 잘 이해하셨을 것입니다. 또한 프레임그래프는 Qt 3D 에서 기본으로 제공하는 렌더러와 머티리얼에 얽매이지 않고 자유롭게 사용할 수 있는 도구라는 점을 항상 염두에 두어야 합니다.
© 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.