シーングラフ - 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 を使ってしきい値をアニメートし、この float 値を均一なバッファでフラグメントシェーダに渡します。

この例は、OpenGL Under QMLDirect3D 11 Under QMLMetal 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> をインクルードしています。

カスタムレンダリングをアンダーレイ/オーバーレイとして追加することは、Qt Quick シーンにカスタム 2D/3D レンダリングを統合する 3 つの方法の 1 つです。他の2つのオプションは、QSGRenderNode を使用して Qt Quick シーン自身のレンダリングと「インライン」でレンダリングを実行するか、専用のレンダーターゲット(テクスチャ)をターゲットとする全く別のレンダーパスを生成し、シーン内のアイテムにテクスチャを表示させることです。これらのアプローチについては、Scene Graph - RHI Texture ItemScene Graph - Custom QSGRenderNodeの例を参照してください。

コア コンセプト

beforeRendering() シグナルは、シーングラフがレンダリングを開始する前に、各フレームの開始時に発せられます。従って、このシグナルへの応答として行われるQRhi draw 呼び出しは、Qt Quick アイテムの下でスタックします。アプリケーション独自のQRhi コマンドは、シーングラフが使用する同じコマンドバッファに記録されるべきです。さらに、コマンドは同じレンダーパスに属するべきです。beforeRendering()は、QRhiCommandBuffer::beginPass()を介してレンダーパスの記録を開始する前に、フレームの開始時に発行されるため、これだけでは十分ではありません。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 ()が発行されると、QRhiBufferQRhiGraphicsPipeline 、関連オブジェクトなど、カスタムレンダリングに必要なQRhi リソースが作成されます(まだ作成されていない場合)。

QRhiResourceUpdateBatchQRhiCommandBuffer::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が使用するQRhiQRhiSwapChain オブジェクトにアクセスするには、QQuickWindow からクエリするだけです。これは、QQuickWindow が通常の画面上のウィンドウであることを前提としていることに注意してください。例えば、テクスチャへのオフスクリーン・レンダリングを行うためにQQuickRenderControl を使用した場合、スワップチェーンは存在しないので、スワップチェーンへの問い合わせは間違っていることになります。

Qt Quick がQRhi::beginFrame() を呼び出した後にシグナルが出力されるため、スワップチェーンからコマンドバッファとレンダーターゲットを照会することができます。このため、QRhiSwapChain::currentFrameCommandBuffer() から返されたオブジェクトに対してQRhiCommandBuffer::resourceUpdate() を発行すると便利です。グラフィック・パイプラインを作成する場合、QRhiSwapChain::currentFrameRenderTarget ()から返されたQRhiRenderTarget から、QRhiRenderPassDescriptor を取得することができます。compatible (テクスチャにレンダリングしたい場合は、テクスチャとスワップチェー ンのフォーマットが異なる可能性があるため、別のQRhiRenderPassDescriptor 、つまり別のグ ラフィックスパイプラインが必要になります。)

void SquircleRenderer::frameStart()
{
    // This function is invoked on the render thread, if there is one.

    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 floats
        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::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::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シェーダーツールビルドシステム統合を参照してください。

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

Scene Graph - RHI Texture ItemおよびScene Graph - Custom QSGRenderNodeも参照してください

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。