Qt 3D 프레임그래프 렌더링
Qt 3D 의 렌더링 기능은 렌더링 알고리즘이 완전히 데이터 중심적으로 작동할 수 있도록 합니다. 이를 제어하는 데이터 구조를 프레임그래프라고 합니다. Qt 3D 의 ECS(엔티티 컴포넌트 시스템)가 엔티티와 컴포넌트의 트리를 통해 장면을 구성함으로써 소위 ‘씬그래프(Scenegraph)’를 정의할 수 있게 해주는 것과 마찬가지로, 프레임그래프 역시 트리 구조이지만 다른 목적으로 사용됩니다. 즉, 장면이 어떻게 렌더링되는지를 제어하는 데 사용됩니다.
단일 프레임을 렌더링하는 과정에서 3D 렌더러는 상태를 여러 번 변경하게 됩니다. 이러한 상태 변경의 횟수와 성격은 씬 내에 어떤 머티리얼(셰이더, 메쉬 지오메트리, 텍스처, 유니폼 변수)이 존재하는지에 따라 달라질 뿐만 아니라, 어떤 상위 수준의 렌더링 방식을 사용하느냐에 따라서도 달라집니다.
예를 들어, 전통적인 단순한 포워드(forward) 렌더링 방식을 사용하는 것은 디퍼드(deferred) 렌더링 방식을 사용하는 것과는 매우 다릅니다. 반사, 그림자, 다중 뷰포트, 조기 Z-필(early z-fill) 패스와 같은 다른 기능들은 모두 렌더러가 한 프레임 동안 설정해야 하는 상태와 이러한 상태 변화가 발생해야 하는 시점을 모두 변경시킵니다.
Qt Quick 비교해 보면, 《 Qt Quick 》 2의 씬그래프 렌더러는 2의 씬을 그리는 역할을 담당하며, C++로 하드코딩되어 프리미티브 배칭이나 불투명 객체를 먼저 렌더링한 뒤 투명 객체를 렌더링하는 등의 작업을 수행합니다. 《 Qt Quick 》 2의 경우, 이 방식이 모든 요구 사항을 충족하므로 전혀 문제가 되지 않습니다. 위에서 언급한 몇 가지 예시에서 볼 수 있듯이, 사용 가능한 렌더링 방법이 매우 다양하다는 점을 고려할 때, 이러한 고정된 방식의 렌더러는 일반적인 3D 씬에 대해 충분한 유연성을 제공하기 어렵습니다. 또는 렌더러를 모든 경우를 다룰 수 있을 만큼 유연하게 만들 수 있다 하더라도, 지나치게 일반화되어 성능이 저하될 가능성이 높습니다. 설상가상으로, 새로운 렌더링 기법들이 끊임없이 연구되고 있습니다. 따라서 우리는 사용과 유지보수가 간편하면서도 유연하고 확장성이 뛰어난 접근 방식이 필요했습니다. 바로 프레임그래프(framegraph)가 등장한 이유입니다!
프레임그래프의 각 노드는 렌더러가 장면을 렌더링하는 데 사용할 구성의 일부를 정의합니다. 프레임그래프 트리 내 노드의 위치는, 해당 노드를 루트로 하는 서브트리가 렌더링 파이프라인에서 언제, 어디서 활성 구성으로 적용될지를 결정합니다. 나중에 살펴보겠지만, 렌더러는 프레임의 각 지점에서 렌더링 알고리즘에 필요한 상태를 구축하기 위해 이 트리를 탐색합니다.
물론 화면에 단순한 큐브 하나만 렌더링하려는 경우라면 이 방법이 지나치게 복잡하다고 생각할 수도 있습니다. 하지만 조금 더 복잡한 장면을 다루기 시작하면 이 방식이 매우 유용해집니다. 일반적인 경우를 위해 Qt 3D 에서는 바로 사용할 수 있는 프레임그래프 예제를 제공합니다.
몇 가지 예제와 그 결과로 생성된 프레임그래프를 통해 프레임그래프 개념의 유연성을 보여드리겠습니다.
엔티티(Entities)와 컴포넌트(Components)로 구성된 씬그래프(Scenegraph)와 달리, 프레임그래프는 모두 Qt3DRender::QFrameGraphNode의 서브클래스인 중첩된 노드들로만 구성되어 있다는 점에 유의하시기 바랍니다. 이는 프레임그래프 노드들이 가상 세계 내의 시뮬레이션 대상 객체가 아니라, 지원 정보의 역할을 하기 때문입니다.
곧 첫 번째 간단한 프레임그래프를 구성하는 방법을 살펴보겠지만, 그 전에 사용할 수 있는 프레임그래프 노드들을 먼저 소개하겠습니다. 또한 씬그래프 트리와 마찬가지로, QML과 C++ API는 1대1로 대응되므로 여러분이 가장 선호하는 방식을 선택하시면 됩니다. 가독성과 간결성을 위해 이 글에서는 QML API를 선택했습니다.
프레임그래프의 장점은 이러한 간단한 노드 유형들을 조합함으로써, 복잡하고 저수준인 C/C++ 렌더링 코드를 전혀 건드리지 않고도 특정 요구 사항에 맞게 렌더러를 구성할 수 있다는 점입니다.
프레임그래프 규칙
올바르게 작동하는 프레임그래프 트리를 구성하려면, 트리를 탐색하는 방법과 이를 Qt 3D 렌더러에 전달하는 방법에 대한 몇 가지 규칙을 알아야 합니다.
프레임그래프 설정
FrameGraph 트리는 QRenderSettings 컴포넌트의 activeFrameGraph 속성에 할당되어야 하며, 이 QRenderSettings 컴포넌트는 Qt 3D 씬의 루트 엔티티에 속한 컴포넌트입니다. 이를 통해 해당 FrameGraph가 렌더러의 활성 프레임그래프로 지정됩니다. 물론, 이는 QML 속성 바인딩이므로 런타임 중에 활성 프레임그래프(또는 그 일부)를 즉석에서 변경할 수 있습니다. 예를 들어, 실내와 실외 장면에 서로 다른 렌더링 방식을 적용하거나 특정 특수 효과를 활성화 또는 비활성화하고 싶을 때 사용할 수 있습니다.
Entity {
id: sceneRoot
components: RenderSettings {
activeFrameGraph: ... // FrameGraph tree
}
}참고: activeFrameGraph는 QML에서 FrameGraph 컴포넌트의 기본 속성입니다.
Entity {
id: sceneRoot
components: RenderSettings {
... // FrameGraph tree
}
}프레임그래프의 사용 방법
- Qt 3D 렌더러는 프레임그래프 트리에 대해 깊이 우선 탐색을 수행합니다. 탐색이 깊이 우선으로 이루어지기 때문에 노드를 정의하는 순서가 중요하다는 점에 유의하십시오.
- 렌더러가 프레임그래프의 리프 노드에 도달하면, 리프 노드에서 루트 노드까지의 경로에 의해 지정된 모든 상태를 수집합니다. 이는 프레임의 특정 구간을 렌더링하는 데 사용되는 상태를 정의합니다. Qt 3D 의 내부 구조에 관심이 있다면, 이러한 상태의 집합을 RenderView라고 부릅니다.
- RenderView에 포함된 구성을 바탕으로, 렌더러는 렌더링될 시나그래프 내의 모든 엔티티를 수집하고, 이를 바탕으로 RenderCommand 집합을 생성하여 RenderView와 연관시킵니다.
- RenderView와 RenderCommands 집합의 조합은 OpenGL에 제출되기 위해 전달됩니다.
- 이 과정이 프레임그래프의 각 리프 노드에 대해 반복되면 프레임이 완성되며, 렌더러는 QOpenGLContext::swapBuffers()를 호출하여 프레임을 표시합니다.
본질적으로 프레임그래프는 Qt 3D 렌더러를 구성하기 위한 데이터 주도형 방식입니다. 이러한 데이터 주도형 특성 덕분에 런타임에 구성을 변경할 수 있고, C++ 개발자가 아닌 개발자나 디자이너도 프레임 구조를 변경할 수 있으며, 수천 줄에 달하는 상용구 코드를 작성하지 않고도 새로운 렌더링 방식을 시도해 볼 수 있습니다.
프레임그래프 예제
프레임그래프 트리를 작성할 때 준수해야 할 규칙을 파악했으니, 이제 몇 가지 예제를 살펴보고 자세히 분석해 보겠습니다.
간단한 포워드 렌더러
포워드 렌더링은 OpenGL을 전통적인 방식으로 사용하여, 각 오브젝트에 셰이딩을 적용하면서 한 번에 하나의 오브젝트씩 백버퍼에 직접 렌더링하는 방식입니다. 이는 중간 G-버퍼에 렌더링하는 디퍼드 렌더링과 대조됩니다. 다음은 포워드 렌더링에 사용할 수 있는 간단한 FrameGraph 예시입니다:
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를 생성합니다:
- 리프 노드 -> RenderView
- 화면 전체를 채우는 뷰포트(중첩된 뷰포트를 쉽게 지원하기 위해 정규화된 좌표를 사용)
- 컬러 및 뎁스 버퍼는 초기화되도록 설정됨
- 노출된 camera 속성에 지정된 카메라
서로 다른 여러 FrameGraph 트리도 동일한 렌더링 결과를 생성할 수 있습니다. 리프에서 루트로 수집된 상태가 동일하다면 결과도 동일합니다. 가장 오랫동안 변하지 않는 상태는 프레임그래프의 루트 가까이에 배치하는 것이 가장 좋습니다. 이렇게 하면 리프 노드의 수가 줄어들고, 결과적으로 전체 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개의 RenderView 객체를 생성합니다. 다음 다이어그램은 처음 두 개의 RenderView 생성 과정을 보여줍니다. 나머지 RenderView들은 두 번째 다이어그램과 매우 유사하며, 단지 다른 서브트리를 사용한다는 점만 다릅니다.


전체적으로 생성된 RenderView는 다음과 같습니다.
- RenderView (1)
- 전체 화면 뷰포트 정의
- 색상 및 깊이 버퍼가 지워지도록 설정됨
- RenderView (2)
- 전체 화면 뷰포트 정의됨
- 하위 뷰포트 정의됨(렌더링 뷰포트는 상위 뷰포트에 상대적으로 크기가 조정됨)
- CameraSelector가 지정되었습니다
- RenderView (3)
- 전체 화면 뷰포트 정의됨
- 하위 뷰포트 정의됨(렌더링 뷰포트는 상위 뷰포트에 상대적으로 크기가 조정됨)
- CameraSelector 지정됨
- RenderView (4)
- 전체 화면 뷰포트 정의됨
- 하위 뷰포트 정의됨(렌더링 뷰포트는 상위 뷰포트에 상대적으로 크기가 조정됨)
- CameraSelector 지정됨
- RenderView (5)
- 전체 화면 뷰포트 정의됨
- 하위 뷰포트 정의됨(렌더링 뷰포트는 상위 뷰포트에 상대적으로 크기가 조정됨)
- CameraSelector 지정됨
하지만 이 경우 순서가 중요합니다. 만약 ClearBuffers 노드가 첫 번째가 아닌 마지막에 위치한다면, 정성스럽게 렌더링된 직후 모든 내용이 지워지기 때문에 화면이 검게 표시되는 결과가 초래될 것입니다. 비슷한 이유로, 이 노드는 FrameGraph의 루트로 사용할 수 없습니다. 그렇게 하면 각 뷰포트마다 화면 전체를 지우는 호출이 발생하게 되기 때문입니다.
FrameGraph의 선언 순서는 중요하지만, Qt 3D 는 각 RenderView가 RenderView의 상태가 유효한 동안 제출될 RenderCommand 세트를 생성하는 데 있어 다른 RenderView와 독립적이기 때문에 각 RenderView를 병렬로 처리할 수 있습니다.
Qt 3D xml-ph-0000@deepl.internal는 사용 가능한 코어 수에 따라 자연스럽게 확장되는 작업 기반 병렬 처리 방식을 사용합니다. 이는 앞선 예제에 대한 다음 다이어그램에서 확인할 수 있습니다.

RenderView에 대한 RenderCommand는 여러 코어에서 병렬로 생성될 수 있으며, 전용 OpenGL 제출 스레드에서 RenderView를 올바른 순서대로 제출하기만 하면 결과 장면이 올바르게 렌더링됩니다.
지연 렌더러
렌더링과 관련하여, 디퍼드 렌더링은 포워드 렌더링과 비교했을 때 렌더러 구성 측면에서 상당히 다른 방식을 취합니다. 디퍼드 렌더링은 각 메시를 그리는 대신 셰이더 효과를 적용하여 음영을 처리하는 대신, 두 단계의 렌더 패스 방식을 채택합니다.
먼저, 씬 내의 모든 메쉬는 동일한 셰이더를 사용하여 그려지며, 이 셰이더는 일반적으로 각 프래그먼트에 대해 최소 네 가지 값을 출력합니다:
- 월드 노멀 벡터
- 색상(또는 기타 머티리얼 속성)
- 깊이
- 월드 위치 벡터
이 값들은 각각 텍스처에 저장됩니다. 노멀, 색상, 깊이, 위치 텍스처는 이른바 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에서 발췌한 것입니다.)
그래프적으로 보면, 결과 프레임그래프는 다음과 같습니다:

그리고 결과적인 RenderView는 다음과 같습니다:
- RenderView (1)
- 사용할 카메라 지정
- 화면 전체를 채우는 뷰포트 정의
- layer component sceneLayer에 속한 모든 엔티티를 선택
gBuffer를 활성 렌더 타깃으로 설정- 현재 바인딩된 렌더 타겟(
gBuffer)의 색상과 깊이 지우기 - RenderPassFilter의 주석과 일치하는 머티리얼 및 테크닉을 가진 엔티티만 선택합니다
- RenderView (2)
- 화면 전체를 채우는 뷰포트를 정의합니다.
- 레이어 컴포넌트 screenQuadLayer에 속한 모든 엔티티를 선택합니다.
- 현재 바인딩된 프레임 버퍼(화면)의 색상 및 깊이 버퍼를 지웁니다.
- RenderPassFilter의 어노테이션과 일치하는 머티리얼 및 테크닉을 가진 씬 내 엔티티만 선택합니다
프레임그래프의 기타 이점
프레임그래프 트리는 전적으로 데이터 기반이며 런타임에 동적으로 수정할 수 있으므로 다음과 같은 작업이 가능합니다:
- 플랫폼 및 하드웨어에 따라 서로 다른 프레임그래프 트리를 준비하고 런타임에 가장 적합한 것을 선택할 수 있습니다
- 씬에서 시각적 디버깅을 쉽게 추가하고 활성화할 수 있습니다
- 씬의 특정 영역에 대해 렌더링해야 할 대상의 특성에 따라 서로 다른 프레임그래프 트리를 사용할 수 있습니다
- Qt 3D 의 내부 구조를 수정하지 않고도 새로운 렌더링 기법을 구현할 수 있습니다
결론
지금까지 FrameGraph와 이를 구성하는 노드 유형들을 살펴보았습니다. 이어서 프레임그래프 구축 규칙과 Qt 3D 엔진이 내부적으로 프레임그래프를 사용하는 방식을 설명하기 위해 몇 가지 예제를 살펴보았습니다. 이제 여러분은 프레임그래프와 그 사용 방법(예를 들어, 포워드 렌더러에 초기 z-fill 패스를 추가하는 등)에 대해 꽤 잘 파악하셨을 것입니다. 또한 FrameGraph는 여러분이 사용할 수 있는 도구이므로, Qt 3D 에서 기본으로 제공하는 렌더러와 머티리얼에만 얽매이지 않도록 항상 염두에 두어야 합니다.
© 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.