씬 그래프 - QML 기반 RHI
Qt Quick (QML) 씬 환경에서 QRhi 를 사용하여 직접 렌더링하는 방법을 보여줍니다.

소개
RHI Under QML 예제는 애플리케이션이 QQuickWindow::beforeRendering() 및 QQuickWindow::beforeRenderPassRecording() 시그널을 활용하여 Qt Quick 장면 아래에 사용자 정의 QRhi 기반 콘텐츠를 그리는 방법을 보여줍니다.
Qt Quick 장면 위에 QRhi 콘텐츠를 렌더링하려는 애플리케이션의 경우, QQuickWindow::beforeRendering()를 사용하여 데이터를 버퍼에 업로드하고 QQuickWindow::afterRenderPassRecording() 시그널에 연결하십시오.
이 예제에서는 QML에 노출된 값이 QRhi 기반 렌더링에 영향을 미치는 방법도 살펴보겠습니다. QML 파일에서 NumberAnimation 을 사용하여 임계값을 애니메이션 처리하면, 이 부동 소수점 값이 유니폼 버퍼를 통해 프래그먼트 셰이더로 전달됩니다.
이 예제는 대부분의 측면에서 ‘QML 기반 OpenGL’, ‘QML 기반 Direct3D 11’, ‘QML 기반 Metal’ 및 ‘QML 기반 Vulkan’ 예제와 동일합니다. 해당 예제들은 3D API를 직접 사용하여 동일한 콘텐츠를 렌더링합니다. 반면, 이 예제는 QRhi 에서 지원하는 모든 3D API(예: OpenGL, Vulkan, Metal, Direct 3D 11 및 12)와의 연동을 본질적으로 지원하므로, 완벽한 크로스 플랫폼 호환성과 이식성을 갖추고 있습니다.
참고: 이 예제는 Qt GUI 모듈에서 호환성이 제한적으로 보장되는 API에 의존하면서도, 이식성 있는 크로스 플랫폼 3D 렌더링을 수행하는 고급 저수준 기능을 보여줍니다. QRhi API를 사용하려면, 애플리케이션이 Qt::GuiPrivate 에 링크되어야 하며 <rhi/qrhi.h> 를 포함해야 합니다.
사용자 정의 렌더링을 언더레이/오버레이로 추가하는 것은 사용자 정의 2D/3D 렌더링을 Qt Quick 씬에 통합하는 세 가지 방법 중 하나입니다. 나머지 두 가지 옵션은 QSGRenderNode 를 사용하여 Qt Quick 씬의 자체 렌더링과 함께 “인라인”으로 렌더링을 수행하거나, 전용 렌더 타겟(텍스처)을 대상으로 완전히 별도의 렌더 패스를 생성한 다음 씬 내의 항목이 해당 텍스처를 표시하도록 하는 것입니다. 이러한 접근 방식에 대해서는 ‘씬 그래프 - RHI 텍스처 아이템’ 및 ‘씬 그래프 - 사용자 정의 QSGRenderNode’ 예제를 참조하십시오.
핵심 개념
beforeRendering() 신호는 매 프레임의 시작 시점, 즉 씬 그래프가 렌더링을 시작하기 전에 발생하므로, 이 신호에 대한 응답으로 수행되는 모든 QRhi 드로우 호출은 Qt Quick 항목 아래에 스택으로 쌓이게 됩니다. 그러나 여기에는 두 가지 관련 신호가 있습니다. 애플리케이션 자체의 ` QRhi ` 명령은 씬 그래프에서 사용하는 것과 동일한 명령 버퍼에 기록되어야 하며, 무엇보다도 해당 명령은 동일한 렌더 패스에 속해야 합니다. beforeRendering()만으로는 이 조건을 충족하기에 충분하지 않습니다. 이 신호는 프레임 시작 시, QRhiCommandBuffer::beginPass()을 통해 렌더 패스 기록을 시작하기 전에 발생하기 때문입니다. beforeRenderPassRecording()에도 연결함으로써, 애플리케이션 자체의 명령어와 씬 그래프 자체의 렌더링이 올바른 순서로 처리됩니다:
- 씬 그래프의 렌더 루프는 ` QRhi::beginFrame()`를 호출합니다
- QQuickWindow::beforeRendering()가 발산되면, 애플리케이션은 사용자 정의 렌더링을 위해 리소스를 준비합니다
- 씬 그래프가 ` QRhiCommandBuffer::beginPass()`를 호출합니다
- QQuickWindow::beforeRenderPassRecording()가 발행됨 - 애플리케이션이 드로우 호출을 기록합니다
- 씬 그래프가 드로우 호출을 기록합니다
단계별 안내
사용자 정의 렌더링은 사용자 정의 ` QQuickItem` 내에 캡슐화되어 있습니다. ` RhiSquircle `은 ` QQuickItem`에서 파생되며, QML에 노출됩니다( QML_ELEMENT 에 유의하십시오). QML 씬은 ` RhiSquircle`를 인스턴스화합니다. 그러나 이는 시각적 항목이 아니라는 점에 유의하십시오. ` QQuickItem::ItemHasContents ` 플래그가 설정되어 있지 않습니다. 따라서 항목의 위치와 크기는 관련이 없으며, ` updatePaintNode()`를 재구현하지 않습니다.
class RhiSquircle : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged)
QML_ELEMENT
public:
RhiSquircle();
qreal t() const { return m_t; }
void setT(qreal t);
signals:
void tChanged();
public slots:
void sync();
void cleanup();
private slots:
void handleWindowChanged(QQuickWindow *win);
private:
void releaseResources() override;
qreal m_t = 0;
SquircleRenderer *m_renderer = nullptr;
};대신, 이 항목이 QQuickWindow 와 연관되면 QQuickWindow::beforeSynchronizing() 신호에 연결됩니다. Qt::DirectConnection 를 사용하는 것이 중요한 이유는, 이 신호가 Qt Quick 렌더링 스레드(존재할 경우)에서 방출되기 때문입니다. 연결된 슬롯이 동일한 스레드에서 호출되도록 해야 합니다.
RhiSquircle::RhiSquircle()
{
connect(this, &QQuickItem::windowChanged, this, &RhiSquircle::handleWindowChanged);
}
void RhiSquircle::handleWindowChanged(QQuickWindow *win)
{
if (win) {
connect(win, &QQuickWindow::beforeSynchronizing, this, &RhiSquircle::sync, Qt::DirectConnection);
connect(win, &QQuickWindow::sceneGraphInvalidated, this, &RhiSquircle::cleanup, Qt::DirectConnection);
// Ensure we start with cleared to black. The squircle's blend mode relies on this.
win->setColor(Qt::black);
}
}씬 그래프의 동기화 단계에서는, 아직 생성되지 않았다면 렌더링 인프라가 생성되고, 렌더링에 필요한 데이터가 동기화됩니다. 즉, 메인 스레드에 존재하는 RhiSquircle 항목에서 렌더 스레드에 존재하는 SquircleRenderer 객체로 데이터가 복사됩니다. (렌더 스레드가 없는 경우, 두 객체 모두 메인 스레드에서 실행됩니다.) 렌더 스레드가 동기화 단계를 실행하는 동안 메인 스레드는 차단되므로 데이터에 안전하게 접근할 수 있습니다. 씬 그래프의 스레딩 및 렌더링 모델에 대한 자세한 내용은 Qt Quick Scene Graph를 참조하십시오.
t 의 값 외에도, 연관된 QQuickWindow 포인터도 함께 복사됩니다. SquircleRenderer 는 렌더 스레드에서 작업 중일 때에도 RhiSquircle 항목에 대해 window()를 쿼리할 수 있지만, 이론적으로는 이것이 완전히 안전한 것은 아닙니다. 따라서 복사본을 생성합니다.
SquircleRenderer 를 설정할 때, beforeRendering() 및 beforeRenderPassRecording()에 대한 연결이 수립되는데, 이는 적절한 시점에 애플리케이션의 사용자 정의 3D 렌더링 명령을 실행하고 삽입할 수 있게 하는 핵심 요소입니다.
void RhiSquircle::sync()
{
// This function is invoked on the render thread, if there is one.
if (!m_renderer) {
m_renderer = new SquircleRenderer;
// Initializing resources is done before starting to record the
// renderpass, regardless of wanting an underlay or overlay.
connect(window(), &QQuickWindow::beforeRendering, m_renderer, &SquircleRenderer::frameStart, Qt::DirectConnection);
// Here we want an underlay and therefore connect to
// beforeRenderPassRecording. Changing to afterRenderPassRecording
// would render the squircle on top (overlay).
connect(window(), &QQuickWindow::beforeRenderPassRecording, m_renderer, &SquircleRenderer::mainPassRecordingStart, Qt::DirectConnection);
}
m_renderer->setT(m_t);
m_renderer->setWindow(window());
}beforeRendering()가 발송되면, QRhiBuffer, QRhiGraphicsPipeline 및 관련 객체와 같이 사용자 정의 렌더링에 필요한 QRhi 리소스가 아직 생성되지 않은 경우 생성됩니다.
버퍼 내의 데이터는 QRhiResourceUpdateBatch 및 QRhiCommandBuffer::resourceUpdate()를 사용하여 업데이트됩니다(더 정확하게는 데이터 업데이트 작업이 큐에 추가됩니다). 버텍스 버퍼는 초기 버텍스 세트가 업로드된 후에는 그 내용이 변경되지 않습니다. 반면 유니폼 버퍼는 이러한 버퍼의 일반적인 특성대로 dynamic 버퍼입니다. 이 버퍼의 내용, 적어도 일부 영역은 매 프레임마다 업데이트됩니다. 따라서 오프셋 0과 바이트 크기 4(C++의 ` float ` 유형이 GLSL의 32비트 ` float`와 일치하므로 ` sizeof(float) `에 해당함)에 대해 ` updateDynamicBuffer()`를 무조건 호출합니다. 해당 위치에 저장된 값은 t 의 값이며, 이는 매 프레임마다, 즉 frameStart()가 호출될 때마다 업데이트됩니다.
버퍼에는 오프셋 4부터 시작하는 추가적인 float 값이 하나 더 있습니다. 이는 3D API 간의 좌표계 차이를 처리하기 위해 사용됩니다: isYUpInNDC()가 false 를 반환할 때(특히 Vulkan의 경우), 이 값은 -1.0으로 설정됩니다. 이로 인해 색상 계산의 기초가 되는, (보간을 거쳐) 프래그먼트 셰이더로 전달되는 2성분 벡터의 Y 값이 반전됩니다. 이렇게 하면 어떤 3D API를 사용하든 화면상의 출력 결과는 동일하게 나타납니다(즉, 왼쪽 상단 모서리는 녹색 계열, 왼쪽 하단 모서리는 빨간색 계열로 표시됨). 이 값은 버텍스 버퍼와 마찬가지로 유니폼 버퍼에서 단 한 번만 업데이트됩니다. 이는 이식성을 목표로 하는 저수준 렌더링 코드가 종종 처리해야 하는 문제, 즉 정규화된 장치 좌표(NDC)와 이미지 및 프레임버퍼 간의 좌표계 차이를 잘 보여줍니다. 예를 들어, NDC는 Vulkan을 제외한 모든 곳에서 좌하단을 원점으로 하는 좌표계를 사용합니다. 반면 프레임버퍼는 OpenGL을 제외한 모든 곳에서 좌상단을 원점으로 하는 좌표계를 사용합니다. 원근 투영을 사용하는 일반적인 렌더러는 ` QRhi::clipSpaceCorrMatrix()` 함수를 편리하게 활용함으로써 이 문제를 간과할 수 있습니다. 이 함수는 투영 행렬에 곱할 수 있는 행렬로, 필요할 때 Y 축 반전을 적용할 뿐만 아니라, 클립 공간의 깊이가 OpenGL에서는 ` -1..1 `로, 그 외 모든 곳에서는 ` 0..1 `로 처리된다는 사실도 고려합니다. 그러나 이 예제와 같은 일부 경우에는 이 방법이 적용되지 않습니다. 대신, 애플리케이션과 셰이더 로직은 QRhi::isYUpInNDC() 및 QRhi::isYUpInFramebuffer()을 조회하여 정점 및 UV 위치에 필요한 조정을 적절히 수행해야 합니다.
Qt Quick 가 사용하는 QRhi 및 QRhiSwapChain 객체에 접근하려면, QQuickWindow 에서 간단히 쿼리하면 됩니다. 이때 QQuickWindow 가 일반적인 온스크린 창이라는 전제 하에 이루어진다는 점에 유의하십시오. 만약 QQuickRenderControl 를 사용하는 경우(예: 텍스처로 오프스크린 렌더링을 수행하는 경우), 해당 시점에는 스왑체인이 존재하지 않으므로 스왑체인을 쿼리하는 것은 잘못된 것입니다.
Qt Quick 가 QRhi::beginFrame()를 호출한 후에 신호가 발생하기 때문에, 이미 스왑체인에서 명령 버퍼와 렌더 타겟을 조회할 수 있습니다. 덕분에 QRhiSwapChain::currentFrameCommandBuffer()에서 반환된 객체에 대해 QRhiCommandBuffer::resourceUpdate()을 편리하게 호출할 수 있습니다. 그래픽 파이프라인을 생성할 때, QRhiSwapChain::currentFrameRenderTarget()에서 반환된 QRhiRenderTarget 을 통해 QRhiRenderPassDescriptor 을 가져올 수 있습니다. (이는 여기서 구축된 그래픽 파이프라인이 스왑체인(swapchain)으로만 렌더링하거나, 기껏해야 해당 스왑체인과 호환되는( compatible ) 다른 렌더 타깃으로만 렌더링하는 데 적합함을 의미합니다. 텍스처로 렌더링하려는 경우, 텍스처와 스왑체인의 형식이 다를 수 있으므로 다른 스왑체인( QRhiRenderPassDescriptor)과 그에 따른 다른 그래픽 파이프라인이 필요할 가능성이 높습니다.)
void SquircleRenderer::frameStart()
{
// 이 함수는 렌더 스레드가 존재하는 경우 해당 스레드에서 호출됩니다.
QRhi*rhi = m_window->rhi();
if (!rhi) {
qWarning("QQuickWindow is not using QRhi for rendering");
return;
}
QRhiSwapChain*swapChain = m_window->swapChain();
if (!swapChain) {
qWarning("No QRhiSwapChain?");
return;
}
QRhiResourceUpdateBatch*resourceUpdates = rhi->nextResourceUpdateBatch();
if (!m_pipeline) {
m_vertexShader = getShader(QLatin1String(":/scenegraph/rhiunderqml/squircle_rhi.vert.qsb"));
if (!m_vertexShader.isValid())
qWarning("Failed to load vertex shader; rendering will be incorrect");
m_fragmentShader = getShader(QLatin1String(":/scenegraph/rhiunderqml/squircle_rhi.frag.qsb"));
if (!m_fragmentShader.isValid())
qWarning("Failed to load fragment shader; rendering will be incorrect");
m_vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
m_vertexBuffer->create();
resourceUpdates->uploadStaticBuffer(m_vertexBuffer.get(), vertices);
const quint32 UBUF_SIZE = 4 + 4; // 2개의 부동소수점 수
m_uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE));
m_uniformBuffer->create();
float yDir = rhi->isYUpInNDC() ? 1.0f:-1.0f;
resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 4, 4, &yDir);
m_srb.reset(rhi->newShaderResourceBindings());
const auto visibleToAll = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
m_srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, visibleToAll, m_uniformBuffer.get())
});
m_srb->create();
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({
{ 2 * sizeof(float) }
});
inputLayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float2, 0 }
});
m_pipeline.reset(rhi->newGraphicsPipeline());
m_pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
QRhiGraphicsPipeline::TargetBlendblend;
blend.enable = true;
blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
blend.srcAlpha = QRhiGraphicsPipeline::SrcAlpha;
blend.dstColor = QRhiGraphicsPipeline::One;
blend.dstAlpha = QRhiGraphicsPipeline::One;
m_pipeline->setTargetBlends({ blend });
m_pipeline->setShaderStages({
{ QRhiShaderStage::Vertex, m_vertexShader },
{ QRhiShaderStage::Fragment, m_fragmentShader }
});
m_pipeline->setVertexInputLayout(inputLayout);
m_pipeline->setShaderResourceBindings(m_srb.get());
m_pipeline->setRenderPassDescriptor(swapChain->currentFrameRenderTarget()->renderPassDescriptor());
m_pipeline->create();
}
float t = m_t;
resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 0, 4, &t);
swapChain->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates);
}마지막으로, ` QQuickWindow::beforeRenderPassRecording()` 호출 시 4개의 정점을 가진 트라이앵글 스트립에 대한 드로우 콜이 기록됩니다. 이 예제는 실제로는 단순히 사각형을 그리는 것이며, 프래그먼트 셰이더의 로직을 사용하여 픽셀 색상을 계산하지만, 애플리케이션에서는 더 복잡한 그리기 작업을 수행할 수도 있습니다. 여러 그래픽 파이프라인을 생성하고 여러 드로우 콜을 기록하는 것도 전혀 문제없습니다. 유념해야 할 중요한 점은, 창의 ` swapchain`에서 가져온 ` QRhiCommandBuffer `에 무엇이 기록되든, 이는 메인 렌더 패스 내에서 ` Qt Quick ` 장면 그래프 자체의 렌더링 앞에 효과적으로 추가된다는 것입니다.
참고: 이는 깊이 테스트를 동반한 깊이 버퍼 사용이나 깊이 값 쓰기 작업이 포함될 경우, Qt Quick 의 내용이 깊이 버퍼에 기록된 값의 영향을 받을 수 있음을 의미합니다. 씬 그래프의 렌더러에 대한 자세한 내용, 특히 불투명 및 알파 블렌딩된 프리미티브 처리에 관한 내용은 Qt Quick Scene Graph Default Renderer를 참조하십시오.
창 크기를 픽셀 단위로 얻으려면 ` QRhiRenderTarget::pixelSize()`를 사용합니다. 이 방법은 예제에서 다른 방법으로 뷰포트 크기를 계산할 필요가 없고, ` high DPI scale factor`가 있는 경우 이를 적용하는 것에 대해 걱정할 필요가 없기 때문에 편리합니다.
void SquircleRenderer::mainPassRecordingStart()
{
// This function is invoked on the render thread, if there is one.
QRhi *rhi = m_window->rhi();
QRhiSwapChain *swapChain = m_window->swapChain();
if (!rhi || !swapChain)
return;
const QSize outputPixelSize = swapChain->currentFrameRenderTarget()->pixelSize();
QRhiCommandBuffer *cb = m_window->swapChain()->currentFrameCommandBuffer();
cb->setViewport({ 0.0f, 0.0f, float(outputPixelSize.width()), float(outputPixelSize.height()) });
cb->setGraphicsPipeline(m_pipeline.get());
cb->setShaderResources();
const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuffer.get(), 0);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(4);
}버텍스 및 프래그먼트 셰이더는 표준 QRhi 셰이더 처리 파이프라인을 거칩니다. 처음에는 Vulkan 호환 GLSL로 작성된 후 SPIR-V로 컴파일되고, Qt 도구를 통해 다른 셰이딩 언어로 트랜스파일됩니다. CMake를 사용할 때, 이 예제는 qt_add_shaders 명령을 활용합니다. 이 명령을 사용하면 셰이더를 애플리케이션에 번들로 묶고 빌드 시점에 필요한 처리를 간단하고 편리하게 수행할 수 있습니다. 자세한 내용은 qt_add_shaders()를 참조하십시오.
BASE 를 지정하면 ../shared 접두사를 제거하는 데 도움이 되며, PREFIX 를 지정하면 의도한 /scenegraph/rhiunderqml 접두사가 추가됩니다. 따라서 최종 경로는 :/scenegraph/rhiunderqml/squircle_rhi.vert.qsb 가 됩니다.
qt_add_shaders(rhiunderqml "rhiunderqml_shaders"
PRECOMPILE
OPTIMIZED
PREFIX
/scenegraph/rhiunderqml
BASE
../shared
FILES
../shared/squircle_rhi.vert
../shared/squircle_rhi.frag
)qmake를 지원하기 위해, 이 예제에서는 일반적으로 빌드 시 생성되는 .qsb 파일을 여전히 포함하고 있으며, 이를 qrc 파일에 나열합니다. 그러나 CMake를 빌드 시스템으로 사용하는 새로운 애플리케이션의 경우 이 접근 방식은 권장되지 않습니다.
‘씬 그래프 - RHI 텍스처 항목 ’ 및 ‘씬 그래프 - 사용자 정의 QSGRenderNode’항목도 참조하십시오 .
© 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.