シーングラフ - QMLにおけるRHI
Qt Quick シーン下でQRhi を使って直接レンダリングする方法を示します。
はじめに
RHI Under QMLの例では、アプリケーションがQQuickWindow::beforeRendering ()とQQuickWindow::beforeRenderPassRecording ()のシグナルを利用して、Qt Quick シーンの下にQRhi-based のカスタムコンテンツを描画する方法を示しています。
Qt Quick シーンの上にQRhi コンテンツを描画したいアプリケーションは、QQuickWindow::afterRendering() とQQuickWindow::afterRenderPassRecording() シグナルに接続することで描画することができます。
この例では、QRhi ベースのレンダリングに影響を与えるような値を QML に公開することも可能であることを説明します。QML ファイルでNumberAnimation を使ってしきい値をアニメートし、この float 値を均一なバッファでフラグメントシェーダに渡します。
この例は、OpenGL Under QML、Direct3D 11 Under QML、Metal Under QML、およびVulkan Under QMLの例とほとんどの点で同等です。これらの例は、3D API を直接使用して同じコンテンツをレンダリングします。一方、この例は、QRhi (OpenGL、Vulkan、Metal、Direct 3D 11および12など)でサポートされているすべての3D APIでの動作を本質的にサポートしているため、完全にクロスプラットフォームで移植可能です。
注意: この例では、Qt GUIモジュールの互換性保証が限定的なAPIに依存しながら、ポータブルでクロスプラットフォームな3Dレンダリングを実行する、高度で低レベルな機能を示しています。QRhi API を使用できるように、アプリケーションはQt::GuiPrivate
にリンクし、<rhi/qrhi.h>
をインクルードしています。
カスタム レンダリングをアンダーレイ/オーバーレイとして追加することは、カスタム 2D/3D レンダリングをQt Quick シーンに統合する 3 つの方法の 1 つです。他の2つのオプションは、QSGRenderNode を使用して、Qt Quick シーン自身のレンダリングと「インライン」でレンダリングを実行するか、専用のレンダー ターゲット(テクスチャ)をターゲットとするまったく別のレンダー パスを生成し、シーン内のアイテムにテクスチャを表示させることです。これらのアプローチに関しては、Scene Graph - RHI Texture ItemとScene Graph - Custom QSGRenderNodeの例を参照してください。
コア コンセプト
beforeRendering() シグナルは、シーングラフがレンダリングを開始する前に、各フレームの開始時に発せられます。従って、このシグナルへの応答として行われるQRhi draw 呼び出しは、Qt Quick アイテムの下にスタックします。アプリケーション自身のQRhi コマンドは、シーングラフによって使用される同じコマンドバッファに記録されるべきで、さらに、コマンドは同じレンダーパスに属するべきです。QRhiCommandBuffer::beginPass ()を介してレンダーパスの記録を開始する前に、フレームの開始時に発行されるため、beforeRendering()だけでは十分ではありません。beforeRenderPassRecording()にも接続することで、アプリケーション自身のコマンドとシーングラフ自身のレンダリングが正しい順序で終了します:
- シーングラフのレンダリングループは、QRhi::beginFrame ()を呼び出します。
- QQuickWindow::beforeRendering(アプリケーションは、カスタムレンダリングのためのリソースを準備します。
- シーングラフがQRhiCommandBuffer::beginPass ()を呼び出す。
- QQuickWindow::beforeRenderPassRecording(アプリケーションは描画コールを記録します。
- シーングラフは描画コールを記録する
ウォークスルー
QQuickItem RhiSquircle
は から派生し、QML に公開されます( に注意)。QMLシーンは をインスタンス化します。 フラグは設定されていません。したがって、アイテムの位置とサイズは関係なく、 () を再実装することもありません。QQuickItem QML_ELEMENT
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
オブジェクトにコピーされる)。(レンダースレッドがない場合は、両方のオブジェクトがメインスレッドに存在する)レンダースレッドがsynchronizeフェーズを実行している間、メインスレッドはブロックされるため、データへのアクセスは安全である。シーン グラフのスレッド化とレンダリング モデルの詳細については、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 から始まる追加の浮動小数点値があります。これは、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 、つまり別のグラフィックパイプラインが必要になる可能性があります)。
voidSquircleRenderer::frameStart() {// この関数は、レンダリングスレッドがあれば、そのスレッド上で呼び出されます。 QRhi*rhi = m_window->rhi();if(!rhi) { // この関数はレンダリングスレッドで呼び出されます。 qWarning("QQuickWindow is not using QRhi for rendering"); return; } *swapChain = m_window>swapChain() QRhiSwapChain*swapChain = m_window->swapChain();if(!swapChain) {. qWarning("No QRhiSwapChain?"); return; } *resourceUpdates = rhi->nextRourceUpdateBatch() QRhiResourceUpdateBatch*resourceUpdates = rhii->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);constquint32UBUF_SIZE= 4 + 4;// 2フロートm_uniformBuffer.reset(rhii->newBuffer(QRhiBuffer::Dynamic、 QRhiBuffer::UniformBuffer,UBUF_SIZE)); m_uniformBuffer->create();floatyDir= rhi->isYUpInNDC()? 1.0f:-1.0f; resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 4, 4, &yDir); m_srb.reset(rhii->newShaderResourceBindings());const autovisibleToAll=QRhiShaderResourceBinding::頂点ステージ QRhiShaderResourceBinding::FragmentStage; m_srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0,visibleToAll,m_uniformBuffer.get()) }); m_srb->create(); QRhiVertexInputLayoutinputLayout; inputLayout.setBindings({2 * sizeof(float) } }); inputLayout.setAttributes({0, 0、 QRhiVertexInputAttribute::Float2, 0} }); m_pipeline.reset(rhii->newGraphicsPipeline()); m_pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip); QRhiGraphicsPipeline::TargetBlend blend; blend.enable= true; blend.srcColor=QRhiGraphicsPipelineblend.srcAlpha= ::SrcAlpha; blend.srcColor=true QRhiGraphicsPipelineblend.srcAlpha =::SrcAlpha; blend.dstColor=QRhiGraphicsPipeline::One; blend.dstAlpha=QRhiGraphicsPipeline::One; m_pipeline->setTargetBlends({ blend }); m_pipeline->setShaderStages({ QRhiShaderStage::Vertex,m_vertexShader },{ {::Fragment, m_frame QRhiShaderStage::Fragment,m_fragmentShader } }); m_pipeline->setVertexInputLayout(inputLayout); m_pipeline->setShaderResourceBindings(m_srb.get()); m_pipeline->setRenderPassDescriptor(swapChain->currentFrameRenderTarget()->renderPassDescriptor()); m_pipeline->create(); }floatt=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
コマンドを使用します。このコマンドを使用することで、シェーダーをアプリケーションにバンドルし、ビルド時に必要な処理を行うことができます。詳細はQtShader Tools Build System Integrationを参照してください。
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をビルドシステムとして使用する新しいアプリケーションには推奨されません。
Scene Graph - RHI Texture ItemおよびScene Graph - Custom QSGRenderNodeも参照して ください。
© 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.