씬 그래프 - QML의 RHI
Qt Quick 씬 아래에서 QRhi 로 직접 렌더링하는 방법을 보여줍니다.
소개
RHI Under QML 예제는 애플리케이션이 QQuickWindow::beforeRendering() 및 QQuickWindow::beforeRenderPassRecording() 신호를 사용하여 Qt Quick 장면 아래에서 사용자 지정 QRhi 기반 콘텐츠를 그리는 방법을 보여줍니다.
Qt Quick 장면 위에 QRhi 콘텐츠를 렌더링하려는 애플리케이션은 QQuickWindow::afterRendering() 및 QQuickWindow::afterRenderPassRecording() 신호에 연결하여 렌더링할 수 있습니다.
이 예제에서는 QRhi 기반 렌더링에 영향을 주는 값을 QML에 노출하는 방법도 살펴봅니다. QML 파일에서 NumberAnimation 을 사용하여 임계값을 애니메이션하고 이 실수 값을 균일한 버퍼로 조각 셰이더에 전달합니다.
이 예제는 대부분의 면에서 OpenGL Under QML, Direct3D 11 Under QML, Metal Under QML 및 Vulkan Under QML 예제와 동일합니다. 이러한 예제는 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 명령은 씬 그래프에서 사용하는 것과 동일한 명령 버퍼에 기록되어야 하며, 또한 명령은 동일한 렌더 패스에 속해야 합니다. QRhiCommandBuffer::beginPass()을 통해 렌더 패스를 기록하기 전에 프레임이 시작될 때 방출되므로 beforeRendering() 자체만으로는 충분하지 않습니다. 또한 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 Quick 렌더 스레드(있는 경우)에서 방출되므로 Qt::DirectConnection 를 사용하는 것이 중요합니다. 연결된 슬롯이 이 동일한 스레드에서 호출되기를 원합니다.
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 씬 그래 프를 참조하세요.
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인 경우 무조건 updateDynamicBuffer()를 호출합니다(C++ float
유형이 GLSL의 32비트 float
와 일치하므로 sizeof(float)
입니다). 해당 위치에 저장되는 것은 t
의 값이며, 이는 매 프레임, 즉 frameStart()를 호출할 때마다 업데이트됩니다.
버퍼에는 오프셋 4부터 시작하는 추가 부동 소수점 값이 있습니다. 이는 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 을 검색할 수 있습니다. (여기서 구축된 그래픽 파이프라인은 스왑체인에 렌더링하거나 기껏해야 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?"); 반환; } 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(!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 floatsm_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::버텍스 스테이지 | 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::TargetBlend blend; 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::버텍스, 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 씬 그래프 기본 렌더러를 참조하세요.
창 크기를 픽셀 단위로 가져오려면 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의 도구로 다른 셰이딩 언어로 트랜스파일됩니다. 이 예제에서는 셰이더를 애플리케이션과 번들로 묶고 빌드 시 필요한 처리를 간단하고 편리하게 수행할 수 있는 qt_add_shaders
명령을 사용합니다. 자세한 내용은 Qt Shader Tools 빌드 시스템 통합을 참조하십시오.
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를 빌드 시스템으로 사용하는 새 애플리케이션에는 권장되지 않습니다.
© 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.