QQuickRenderControl RHI の例

Qt Quick シーンをQRhiTexture にレンダリングする方法を示します。

この例では、レンダリングがQRhiTexture にリダイレクトされる Qt Quick シーンのセットアップ方法を示します。この例は、QWidget ベースのアプリケーションで、画像データのリードバックを実行し、収集したフレームごとのレンダリングを、それぞれの CPU および GPU ベースのタイミング情報とともに表示します。

Qtの3DグラフィックスAPIの抽象化を使用することで、この例は特定のグラフィックスAPIに縛られません。起動時に、サポートされる可能性のあるプラットフォームの 3D API を示すダイアログが表示されます。

    QDialog apiSelect;
    QVBoxLayout *selLayout = new QVBoxLayout;
    selLayout->addWidget(new QLabel(QObject::tr("Select graphics API to use")));
    QListWidget *apiList = new QListWidget;
    QVarLengthArray<QSGRendererInterface::GraphicsApi, 5> apiValues;
#ifdef Q_OS_WIN
    apiList->addItem("Direct3D 11");
    apiValues.append(QSGRendererInterface::Direct3D11);
    apiList->addItem("Direct3D 12");
    apiValues.append(QSGRendererInterface::Direct3D12);
#endif
#if QT_CONFIG(metal)
    apiList->addItem("Metal");
    apiValues.append(QSGRendererInterface::Metal);
#endif
#if QT_CONFIG(vulkan)
    apiList->addItem("Vulkan");
    apiValues.append(QSGRendererInterface::Vulkan);
#endif
#if QT_CONFIG(opengl)
    apiList->addItem("OpenGL / OpenGL ES");
    apiValues.append(QSGRendererInterface::OpenGL);
#endif
    if (apiValues.isEmpty()) {
        QMessageBox::critical(nullptr, QObject::tr("No 3D graphics API"), QObject::tr("No 3D graphics APIs are supported in this Qt build"));
        return 1;
    }

注意: すべての選択が特定のプラットフォームで機能することは保証されていません。

選択が行われると、QMLファイルがロードされます。しかし、単純にQQuickView インスタンスを作成し、show ()するわけではありません。むしろ、Qt Quickシーンを管理するQQuickWindow は画面上に表示されません。その代わりに、アプリケーションは、QQuickRenderControl を介して、いつ、どこにレンダリングするかを制御します。

void MainWindow::load(const QString &filename)
{
    reset();

    m_renderControl.reset(new QQuickRenderControl);
    m_scene.reset(new QQuickWindow(m_renderControl.get()));

    // enable lastCompletedGpuTime() on QRhiCommandBuffer, if supported by the underlying 3D API
    QQuickGraphicsConfiguration config;
    config.setTimestamps(true);
    m_scene->setGraphicsConfiguration(config);

#if QT_CONFIG(vulkan)
    if (m_scene->graphicsApi() == QSGRendererInterface::Vulkan)
        m_scene->setVulkanInstance(m_vulkanInstance);
#endif

    m_qmlEngine.reset(new QQmlEngine);
    m_qmlComponent.reset(new QQmlComponent(m_qmlEngine.get(), QUrl::fromLocalFile(filename)));
    if (m_qmlComponent->isError()) {
        for (const QQmlError &error : m_qmlComponent->errors())
            qWarning() << error.url() << error.line() << error;
        QMessageBox::critical(this, tr("Cannot load QML scene"), tr("Failed to load %1").arg(filename));
        reset();
        return;
    }

オブジェクトツリーがインスタンス化されると、ルートアイテム(Rectangle )がクエリされ、そのサイズが有効であることが確認された後、伝搬されます。

注: オブジェクトツリー内でWindow 要素を使用するシーンはサポートされていません。

    QObject *rootObject = m_qmlComponent->create();
    if (m_qmlComponent->isError()) {
        for (const QQmlError &error : m_qmlComponent->errors())
            qWarning() << error.url() << error.line() << error;
        QMessageBox::critical(this, tr("Cannot load QML scene"), tr("Failed to create component"));
        reset();
        return;
    }

    QQuickItem *rootItem = qobject_cast<QQuickItem *>(rootObject);
    if (!rootItem) {
        // Get rid of the on-screen window, if the root object was a Window
        if (QQuickWindow *w = qobject_cast<QQuickWindow *>(rootObject))
            delete w;
        QMessageBox::critical(this,
                              tr("Invalid root item in QML scene"),
                              tr("Root object is not a QQuickItem. If this is a scene with Window in it, note that such scenes are not supported."));
        reset();
        return;
    }

    if (rootItem->size().width() < 16)
        rootItem->setSize(QSizeF(640, 360));

    m_scene->contentItem()->setSize(rootItem->size());
    m_scene->setGeometry(0, 0, rootItem->width(), rootItem->height());

    rootItem->setParentItem(m_scene->contentItem());

    m_statusMsg->setText(tr("QML scene loaded"));

この時点では、レンダリングリソースは初期化されていません。つまり、ネイティブ3DグラフィックスAPIはまだ何も実行されていません。QRhi がインスタンス化されるのは次のステップで、これが、Vulkan、Metal、Direct 3D などのレンダリングシステムをセットアップするトリガーになります。

    const bool initSuccess = m_renderControl->initialize();
    if (!initSuccess) {
        QMessageBox::critical(this, tr("Cannot initialize renderer"), tr("QQuickRenderControl::initialize() failed"));
        reset();
        return;
    }

    const QSGRendererInterface::GraphicsApi api = m_scene->rendererInterface()->graphicsApi();
    switch (api) {
    case QSGRendererInterface::OpenGL:
        m_apiMsg->setText(tr("OpenGL"));
        break;
    case QSGRendererInterface::Direct3D11:
        m_apiMsg->setText(tr("D3D11"));
        break;
    case QSGRendererInterface::Direct3D12:
        m_apiMsg->setText(tr("D3D12"));
        break;
    case QSGRendererInterface::Vulkan:
        m_apiMsg->setText(tr("Vulkan"));
        break;
    case QSGRendererInterface::Metal:
        m_apiMsg->setText(tr("Metal"));
        break;
    default:
        m_apiMsg->setText(tr("Unknown 3D API"));
        break;
    }

    QRhi *rhi = m_renderControl->rhi();
    if (!rhi) {
        QMessageBox::critical(this, tr("Cannot render"), tr("No QRhi from QQuickRenderControl"));
        reset();
        return;
    }

    m_driverInfoMsg->setText(QString::fromUtf8(rhi->driverInfo().deviceName));

注意: このアプリケーションでは、Qt がQRhi のインスタンスを作成するモデルを使用しています。これは、唯一の可能なアプローチではありません。アプリケーションが独自のQRhi (OpenGL コンテキスト、Vulkan デバイスなど)を保持している場合、Qt Quick に既存のQRhi を採用して使用するように要求できます。QQuickGraphicsConfiguration これは、QQuickGraphicsDevice::fromRhi() によって作成されたQQuickGraphicsDeviceQQuickWindow に渡すことによって行われます。例えば、Qt QuickでレンダリングされたテクスチャをQRhiWidget で使用したい場合を考えてみましょう。この場合、Qt Quickに独自のテクスチャを作成させるのではなく、QRhiWidgetQRhi をQt Quickに渡す必要があります。

QQuickRenderControl::initialize() が成功したら、レンダラーが稼動して準備完了です。そのためには、レンダリングするためのカラー・バッファが必要です。

QQuickRenderTarget は軽量な暗黙的に共有されるクラスで、テクスチャやレンダーターゲットなどを記述するさまざまなネイティブオブジェクトや オブジェクトのセットを保持します(所有はしません)。 で () を呼び出すと、Qt Quick シーングラフのレンダリングがアプリケーションから提供されたテクスチャにリダイレクトされます。 (OpenGLのテクスチャIDやVkImageオブジェクトのようなネイティブの3D APIオブジェクトではありません)を使用する場合、アプリケーションは を設定し、 ()を介してQt Quickに渡す必要があります。QRhi QQuickWindow QQuickWindow setRenderTarget QRhi QRhiTextureRenderTarget QQuickRenderTarget::fromRhiRenderTarget

    const QSize pixelSize = rootItem->size().toSize(); // no scaling, i.e. the item size is in pixels

    m_texture.reset(rhi->newTexture(QRhiTexture::RGBA8, pixelSize, 1,
                                    QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
    if (!m_texture->create()) {
        QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create texture object"));
        reset();
        return;
    }

    m_ds.reset(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, 1));
    if (!m_ds->create()) {
        QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create depth-stencil buffer"));
        reset();
        return;
    }

    QRhiTextureRenderTargetDescription rtDesc(QRhiColorAttachment(m_texture.get()));
    rtDesc.setDepthStencilBuffer(m_ds.get());
    m_rt.reset(rhi->newTextureRenderTarget(rtDesc));
    m_rpDesc.reset(m_rt->newCompatibleRenderPassDescriptor());
    m_rt->setRenderPassDescriptor(m_rpDesc.get());
    if (!m_rt->create()) {
        QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create render target"));
        reset();
        return;
    }

    m_scene->setRenderTarget(QQuickRenderTarget::fromRhiRenderTarget(m_rt.get()));

注意: レンダリング時に Qt Quick のシーングラフによって、これらのバッファと深度およびステンシル・テストの両方が使用される可能性があるため、Qt Quick には常に深度-ステンシル・バッファを提供してください。

メインのレンダリングループは次のようになります。これは、画像の GPU->CPU リードバックを実行する方法も示しています。QImage が利用可能になると、それに応じてQWidget ベースのユーザーインターフェイスが更新されます。ここでは、その詳細については省略します。

この例では、CPUとGPUでフレームをレンダリングするコストを測定する簡単な方法も示しています。オフスクリーンでレンダリングされたフレームは、QRhi の内部動作により、このような動作に適しています。この動作は、そうでなければ非同期(後続のフレームをレンダリングするときにのみ完了するという意味)である操作が、QRhi::endOffscreenFrame ()(すなわち、QQuickRenderControl::endFrame ())が返された時点で準備ができていることが保証されていることを意味します。この知識はテクスチャを読み返すときに使用し、GPUのタイムスタンプにも適用されます。そのため、アプリケーションは各フレームの GPU 時間を表示することができ、同時にその時間が実際にその特定のフレームを指している(それ以前のものではない)ことを保証することができます。GPU タイミングの詳細についてはlastCompletedGpuTime() を参照してください。CPU 側のタイミングはQElapsedTimer を使って取得します。

    QElapsedTimer cpuTimer;
    cpuTimer.start();

    m_renderControl->polishItems();

    m_renderControl->beginFrame();

    m_renderControl->sync();
    m_renderControl->render();

    QRhi *rhi = m_renderControl->rhi();
    QRhiReadbackResult readResult;
    QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
    readbackBatch->readBackTexture(m_texture.get(), &readResult);
    m_renderControl->commandBuffer()->resourceUpdate(readbackBatch);

    m_renderControl->endFrame();

    const double gpuRenderTimeMs = m_renderControl->commandBuffer()->lastCompletedGpuTime() * 1000.0;
    const double cpuRenderTimeMs = cpuTimer.nsecsElapsed() / 1000000.0;

    // m_renderControl->begin/endFrame() is based on QRhi's
    // begin/endOffscreenFrame() under the hood, meaning it does not do
    // pipelining, unlike swapchain-based frames, and therefore the readback is
    // guaranteed to complete once endFrame() returns.
    QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
                    readResult.pixelSize.width(), readResult.pixelSize.height(),
                    QImage::Format_RGBA8888_Premultiplied);
    QImage result;
    if (rhi->isYUpInFramebuffer())
        result = wrapperImage.mirrored();
    else
        result = wrapperImage.copy();

重要なのは、Qt Quick アニメーションのステップ処理です。経過時間の計測、通常のタイマー、またはプレゼンテーション・レート・ベースのスロットリングのいずれかを使ってアニメーション・システムを駆動できるオンスクリーン・ウィンドウがないため、Qt Quickレンダリングをリダイレクトすると、アニメーションの駆動をアプリケーションに引き継ぐ必要があることがよくあります。そうでない場合、アニメーションは普通のシステムタイマーに基づいて機能しますが、実際の経過時間は、オフスクリーンでレンダリングされたシーンが知覚すると予想されるものとは無関係であることがよくあります。タイトなループで5フレームを連続してレンダリングすることを考えてみよう。その5フレーム内のアニメーションがどのように動くかは、CPUがループの繰り返しを実行する速度に依存する。これはほとんど理想的ではありません。一貫したアニメーションを保証するには、カスタムのQAnimationDriverをインストールしてください。これは文書化されていない(しかし公開されている)上級者向けのAPIですが、ここでは簡単な使用例を示しています。

class AnimationDriver : public QAnimationDriver
{
public:
    AnimationDriver(QObject *parent = nullptr)
        : QAnimationDriver(parent),
          m_step(16)
    {
    }

    void setStep(int milliseconds)
    {
        m_step = milliseconds;
    }

    void advance() override
    {
        m_elapsed += m_step;
        advanceAnimation();
    }

    qint64 elapsed() const override
    {
        return m_elapsed;
    }

private:
    int m_step;
    qint64 m_elapsed = 0;
};

このアプリケーションにはQSlider 、アニメーションのステップ値をデフォルトの16ミリ秒から他の値に変更することができます。QAnimationDriverサブクラスのsetStep()関数の呼び出しに注目してください。

    QSlider *animSlider = new QSlider;
    animSlider->setOrientation(Qt::Horizontal);
    animSlider->setMinimum(1);
    animSlider->setMaximum(1000);
    QLabel *animLabel = new QLabel;
    QObject::connect(animSlider, &QSlider::valueChanged, animSlider, [this, animLabel, animSlider] {
        if (m_animationDriver)
            m_animationDriver->setStep(animSlider->value());
        animLabel->setText(tr("Simulated elapsed time per frame: %1 ms").arg(animSlider->value()));
    });
    animSlider->setValue(16);
    QCheckBox *animCheckBox = new QCheckBox(tr("Custom animation driver"));
    animCheckBox->setToolTip(tr("Note: Installing the custom animation driver makes widget drawing unreliable, depending on the platform.\n"
                                "This is due to widgets themselves relying on QPropertyAnimation and similar, which are driven by the same QAnimationDriver.\n"
                                "In any case, the functionality of the widgets are not affected, just the rendering may lag behind.\n"
                                "When not checked, Qt Quick animations advance based on the system time, i.e. the time elapsed since the last press of the Next button."));
    QObject::connect(animCheckBox, &QCheckBox::stateChanged, animCheckBox, [this, animCheckBox, animSlider, animLabel] {
        if (animCheckBox->isChecked()) {
            animSlider->setEnabled(true);
            animLabel->setEnabled(true);
            m_animationDriver = new AnimationDriver(this);
            m_animationDriver->install();
            m_animationDriver->setStep(animSlider->value());
        } else {
            animSlider->setEnabled(false);
            animLabel->setEnabled(false);
            delete m_animationDriver;
            m_animationDriver = nullptr;
        }
    });
    animSlider->setEnabled(false);
    animLabel->setEnabled(false);
    controlLayout->addWidget(animCheckBox);
    controlLayout->addWidget(animLabel);
    controlLayout->addWidget(animSlider);

注: カスタムアニメーションドライバのインストールは、animCheckBox チェックボックスで任意に設定できます。これにより、カスタムアニメーションドライバをインストールした場合としなかった場合の効果を比較することができます。さらに、プラットフォームによっては(そしておそらくテーマによっては)、カスタムドライバーを有効にすると、ウィジェットの描画に遅延が生じることがあります。なぜなら、ウィジェットのアニメーション(例えば、QPushButtonQCheckBox のハイライト)がQPropertyAnimation などで管理されている場合、それらのアニメーションは同じ QAnimationDriver によって駆動され、ボタンをクリックして新しいフレームが要求されるまで、アニメーションが進まないからです。

アニメーションを進めるのは、単純にadvance()を呼び出すことによって、各フレームの前(つまり、QQuickRenderControl::beginFrame ()呼び出しの前)に行われます:

void MainWindow::stepAnimations()
{
    if (m_animationDriver) {
        // Now the Qt Quick scene will think that <slider value> milliseconds have
        // elapsed and update animations accordingly when doing the next frame.
        m_animationDriver->advance();
    }
}

プロジェクト例 @ code.qt.io

QRhi,QQuickRenderControl,QQuickWindowも参照してください

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