このページでは

シーングラフ - QML における RHI

Qt Quick シーン内でQRhi を使用して直接レンダリングする方法を示します。

Qt Quickのシーン内でQRhiを使用してレンダリングされたアニメーション付きスクエアサークルに、テキストを重ねたもの

はじめに

「RHI Under QML」のサンプルでは、アプリケーションがQQuickWindow::beforeRendering()およびQQuickWindow::beforeRenderPassRecording()シグナルを利用して、Qt Quick シーンの下にQRhi ベースのカスタムコンテンツを描画する方法を示しています。

Qt Quick シーンの上にQRhi コンテンツをレンダリングしたいアプリケーションでは、QQuickWindow::beforeRendering()を使用してデータをバッファにアップロードし、QQuickWindow::afterRenderPassRecording()シグナルに接続してください。

この例では、QMLに公開された値がQRhi ベースのレンダリングに影響を与える仕組みについても説明します。QMLファイル内のNumberAnimation を使用してしきい値をアニメーション化し、このfloat値はユニフォームバッファを介してフラグメントシェーダーに渡されます。

この例は、多くの点で「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> をインクルードする必要があります。

アンダーレイ/オーバーレイとしてカスタムレンダリングを追加することは、Qt Quick シーンにカスタム2D/3Dレンダリングを統合する3つの方法のうちの1つです。 他の 2 つのオプションは、QSGRenderNode を使用してQt Quick シーン独自のレンダリングと「インライン」でレンダリングを行う方法、あるいは専用のレンダリングターゲット(テクスチャ)を対象として完全に独立したレンダリングパスを生成し、シーン内のアイテムでそのテクスチャを表示させる方法です。 これらのアプローチについては、「Scene Graph - RHI Texture Item」および「Scene Graph - Custom QSGRenderNode」のサンプルを参照してください。

基本概念

beforeRendering() シグナルは、各フレームの開始時、シーングラフがレンダリングを開始する前に発火します。したがって、このシグナルに応答して行われるQRhi による描画呼び出しは、Qt Quick アイテムの下にスタックされます。 ただし、ここで関連するシグナルが 2 つあります。アプリケーション独自のQRhi コマンドは、シーングラフで使用されるのと同じコマンドバッファに記録されるべきであり、さらに、それらのコマンドは同じレンダリングパスに属している必要があります。 beforeRendering() だけではこれには不十分です。なぜなら、このシグナルはフレームの開始時に発行され、QRhiCommandBuffer::beginPass() によるレンダリングパスの記録が開始される前だからです。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 に対して、updateDynamicBuffer() が無条件に呼び出されます(C++のfloat 型がたまたまGLSLの32ビットfloat 型と一致するため、これはsizeof(float) に相当します)。 その位置に格納されているのはt の値であり、これは毎フレーム、つまりframeStart()が呼び出されるたびに更新されます。

バッファには、オフセット4から始まる追加のfloat型値が存在します。これは、3D API間の座標系の違いに対応するために使用されます:isYUpInNDC()がfalse を返す場合(特にVulkanではこのケースになります)、この値は-1.0に設定されます。これにより、色の計算の基となるフラグメントシェーダーへ(補間を経て)渡される2成分ベクトルのY値が反転します。 これにより、どの3D APIを使用しているかに関わらず、画面上の出力は同一になります(つまり、左上隅は緑がかった色、左下隅は赤がかった色になります)。 この値は、頂点バッファと同様に、ユニフォームバッファ内で 1 回だけ更新されます。これは、移植性を重視する低レベルのレンダリングコードがしばしば対処しなければならない問題、すなわち、正規化デバイス座標 (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()を便利に発行できるようになります。グラフィックスパイプラインを作成する際、QRhiRenderPassDescriptor はQRhiSwapChain::currentFrameRenderTarget()から返されるQRhiRenderTarget から取得できます。 (ここで構築されたグラフィックスパイプラインは、スワップチェーンへのレンダリング、あるいはせいぜいそれと同じcompatible を持つ別のレンダリングターゲットへのレンダリングにのみ適していることに注意してください。テクスチャへのレンダリングを行う場合、テクスチャとスワップチェーンのフォーマットが異なる可能性があるため、別のQRhiRenderPassDescriptor 、ひいては別のグラフィックスパイプラインが必要になる可能性が高いです)

voidSquircleRenderer::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);

        constquint32 UBUF_SIZE= 4 + 4;// 2つのfloat
        m_uniformBuffer.reset(rhi->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(rhi->newShaderResourceBindings());
        const autovisibleToAll=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();
    }

   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 シーングラフのデフォルトレンダラー」を参照してください。

ウィンドウのサイズをピクセル単位で取得するには、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 をビルドシステムとして使用する新しいアプリケーションでは、このアプローチは推奨されません。

サンプルプロジェクト @ code.qt.io

「シーングラフ - 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.