このページでは

RHIウィジェットの簡単な例

QRhi (Qt 3D APIおよびシェーディング言語の抽象化レイヤー)を使用して三角形を描画する方法を示します。

緑色の背景に描かれた色付きの三角形

「シンプルなRHIウィジェットの例」のスクリーンショット

このサンプルは、多くの点で、QWidget の世界における「RHIウィンドウのサンプル」に相当するものです。 このアプリケーションのQRhiWidget サブクラスは、基本的な頂点シェーダーとフラグメントシェーダーを用いたシンプルなグラフィックスパイプラインを使用して、単一の三角形を描画します。QWindow のみに基づくアプリケーションとは異なり、この例では、ウィンドウやQRhi の設定、スワップチェーンやウィンドウイベントの処理といった低レベルの詳細について気にする必要はありません。これらは、ここではQWidget フレームワークによって処理されるためです。QRhiWidget のサブクラスのインスタンスは、QVBoxLayout に追加されます。例を最小限かつコンパクトに保つため、これ以外のウィジェットや3Dコンテンツは導入していません。

ExampleRhiWidget (QRhiWidget のサブクラス)のインスタンスが、最上位ウィジェットの子階層に追加されると、対応するウィンドウは自動的にDirect 3D、Vulkan、Metal、またはOpenGLでレンダリングされるウィンドウになります。 その後、QPainter でレンダリングされたウィジェットコンテンツ(つまり、QRhiWidget 、QOpenGLWidget 、またはQQuickWidget 以外のすべて)はテクスチャにアップロードされます。一方、前述の特殊なウィジェットはそれぞれ個別のテクスチャにレンダリングされます。その結果生成されたtextures のセットは、トップレベルのウィジェットのバッキングストアによって合成されます。

構造と main()

main() 関数は非常に単純です。トップレベルのウィジェットのデフォルトサイズは720pです(このサイズは論理単位でのものであり、scale factor の設定に応じて実際のピクセルサイズは異なる場合があります)。ウィンドウのサイズは変更可能です。QRhiWidget を使用することで、ウィンドウサイズの変更やレイアウトの変更に伴うウィジェットのサイズ変更を適切に処理するサブクラスを簡単に実装できます。

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    ExampleRhiWidget *rhiWidget = new ExampleRhiWidget;

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(rhiWidget);

    QWidget w;
    w.setLayout(layout);
    w.resize(1280, 720);
    w.show();

    return app.exec();
}

QRhiWidget サブクラスは、initialize() とrender() の2つの仮想関数を再実装しています。 initialize() は、render() の前に少なくとも 1 回呼び出されますが、ウィジェットのサイズ変更によってウィジェットのバッキングテクスチャが再作成されたとき、レンダリングターゲットのパラメータが変更されたとき、あるいは新しいトップレベルウィンドウへの移動によってウィジェットが新しい `QRhi ` に変更されたときなど、いくつかの重要な変更が発生した際にも呼び出されます。

注: QOpenGLWidget の従来のinitializeGL -resizeGL -paintGL モデルとは異なり 、QRhiWidget には仮想関数が2つしかありません。これは、サイズ変更だけでなく、例えば別のトップレベルウィンドウへの親変更など、対処が必要となる可能性のある特別なイベントが他にもあるためです。 (堅牢なQOpenGLWidget 実装では、関連するQOpenGLContext のライフタイムを追跡するなど、追加の管理処理を行う必要がありました。つまり、実際には3つの仮想メソッドだけでは不十分だったのです)重要な変更が発生した際にinitialize を再呼び出しする、initialize とrender というよりシンプルなペアの方が、この用途には適しています。

QRhi インスタンスはウィジェットが所有しているものではありません。これはinitialize() from the base class 内でクエリされます。これをメンバとして格納することで、initialize() が再呼び出された際に変更を認識できるようになります。ただし、頂点バッファやユニフォームバッファ、あるいはグラフィックスパイプラインなどのグラフィックスリソースは、ExampleRhiWidget の管理下にあります。

#include <QRhiWidget>
#include <rhi/qrhi.h>

class ExampleRhiWidget : public QRhiWidget
{
public:
    ExampleRhiWidget(QWidget *parent = nullptr) : QRhiWidget(parent) { }

    void initialize(QRhiCommandBuffer *cb) override;
    void render(QRhiCommandBuffer *cb) override;

private:
    QRhi *m_rhi = nullptr;
    std::unique_ptr<QRhiBuffer> m_vbuf;
    std::unique_ptr<QRhiBuffer> m_ubuf;
    std::unique_ptr<QRhiShaderResourceBindings> m_srb;
    std::unique_ptr<QRhiGraphicsPipeline> m_pipeline;
    QMatrix4x4 m_viewProjection;
    float m_rotation = 0.0f;
};

#include <rhi/qrhi.h> ステートメントが機能するためには、アプリケーションはGuiPrivate (またはqmakeを使用する場合はgui-private )にリンクする必要があります。QRhi APIファミリーの互換性に関する詳細については、QRhi を参照してください。

CMakeLists.txt

target_link_libraries(simplerhiwidget PRIVATE
    Qt6::Core
    Qt6::Gui
    Qt6::GuiPrivate
    Qt6::Widgets
)

レンダリングの設定

examplewidget.cpp では、ウィジェットの実装でヘルパー関数を使用し、.qsb ファイルからQShader オブジェクトを読み込んでいます。 このアプリケーションには、Qtリソースシステムを介して実行ファイルに埋め込まれた、あらかじめ条件設定済みの.qsb ファイルが同梱されています。モジュールの依存関係(およびqmakeのサポートが継続されていること)のため、この例では便利なCMake関数qt_add_shaders() は使用せず、代わりに.qsb ファイルをソースツリーの一部として同梱しています。 実際のアプリケーションでは、この方法を避け、代わりに QtShader Tools モジュールの CMake 統合機能(qt_add_shaders )を使用することを推奨します。いずれのアプローチを採用する場合でも、C++ コード内でのバンドル済み/生成された.qsb ファイルの読み込み方法は同じです。

static QShader getShader(const QString &name)
{
    QFile f(name);
    return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
}

initialize() の実装を見てみましょう。まず、QRhi オブジェクトを照会して後で使用するために保存します。これは、将来この関数が呼び出された際の比較を可能にするためでもあります。 不一致が生じた場合(例えば、ウィジェットがウィンドウ間で移動された場合など)、グラフィックリソースの再作成が必要となります。これは、適切なオブジェクト(この場合はm_pipeline )を破棄し、nullに設定することでトリガーされます。この例では、ウィンドウ間の親ウィジェットの変更を積極的に示してはいませんが、それに対応できるよう準備されています。 また、ウィンドウのサイズ変更時に発生しうるウィジェットのサイズ変化にも対応できるよう準備されています。これは、その都度 `initialize() ` が呼び出されるため、特別な処理は不要です。したがって、`renderTarget()->pixelSize() ` または `colorTexture()->pixelSize() ` を照会すれば、常に最新のサイズ(ピクセル単位)が得られます。 ただし、この例では、color buffer formats およびmultisample settings の変更には対応していません。これらは常にデフォルト値(RGBA8、マルチサンプルアンチエイリアシングなし)のみを使用しているためです。

void ExampleRhiWidget::initialize(QRhiCommandBuffer *cb)
{
    if (m_rhi != rhi()) {
        m_pipeline.reset();
        m_rhi = rhi();
    }

グラフィックスリソースを(再)作成する必要がある場合、initialize() は、ごく一般的なQRhi ベースのコードを使用してこれを行います。位置と色の頂点データがインターリーブされた単一の頂点バッファで十分であり、モデルビュー投影行列は64バイト(16個の浮動小数点数)のユニフォームバッファを介して公開されます。 ユニフォームバッファは、シェーダーから参照可能な唯一のリソースであり、頂点シェーダー内でのみ使用されます。グラフィックスパイプラインは、多くのデフォルト設定に依存しています(例えば、深度テストの無効化、ブレンディングの無効化、カラー書き込みの有効化、面カリングの無効化、三角形のデフォルトのトポロジーなど)。 頂点データのレイアウトは、x 、y 、r 、g 、b であり、したがってストライドは5フロートですが、2番目の頂点入力属性(色)は2フロートのオフセットを持っています(x およびy をスキップ)。各グラフィックスパイプラインは、QRhiRenderPassDescriptor に関連付けられる必要があります。これは、基底クラスが管理するQRhiRenderTarget から取得できます。

注:この例は 、QRhiWidget のデフォルト設定であるautoRenderTarget がtrue に設定されていることを前提としています。そのため、レンダリングターゲットを管理する必要はなく、renderTarget()を呼び出すだけで既存のものを取得できます。

    if (!m_pipeline) {
        m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)));
        m_vbuf->create();

        m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
        m_ubuf->create();

        m_srb.reset(m_rhi->newShaderResourceBindings());
        m_srb->setBindings({
            QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, m_ubuf.get()),
        });
        m_srb->create();

        m_pipeline.reset(m_rhi->newGraphicsPipeline());
        m_pipeline->setShaderStages({
            { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shader_assets/color.vert.qsb")) },
            { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/color.frag.qsb")) }
        });
        QRhiVertexInputLayout inputLayout;
        inputLayout.setBindings({
            { 5 * sizeof(float) }
        });
        inputLayout.setAttributes({
            { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
            { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
        });
        m_pipeline->setVertexInputLayout(inputLayout);
        m_pipeline->setShaderResourceBindings(m_srb.get());
        m_pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
        m_pipeline->create();

        QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
        resourceUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData);
        cb->resourceUpdate(resourceUpdates);
    }

最後に、投影行列が計算されます。これはウィジェットのサイズに依存するため、関数が呼び出されるたびに無条件に実行されます。

注: サイズやビューポートの 計算は、 常にカラーバッファとして機能するリソースから取得したピクセルサイズにのみ基づいて行う必要があります。これは、そのリソースが実際のレンダリングターゲットであるためです。QWidget が報告するサイズやデバイスのピクセル比率に基づいて、サイズ、ビューポート、シザーズなどを手動で計算することは避けてください。

注: 投影行列には 、正規化されたデバイス座標における3D API間の違い(例:Y軸が下向きか上向きか)に対応するため、QRhi から取得したcorrection matrix が含まれています。

-4 に対する並進変換が適用されるのは、z の値が 0 である三角形が確実に表示されるようにするためです。

    const QSize outputSize = renderTarget()->pixelSize();
    m_viewProjection = m_rhi->clipSpaceCorrMatrix();
    m_viewProjection.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f);
    m_viewProjection.translate(0, 0, -4);
}

レンダリング

このウィジェットは、単一の描画呼び出しを含む単一のレンダリングパスを記録します。

初期化ステップで計算されたビュー投影行列は、モデル行列と組み合わされます。この場合は、単純な回転となります。その結果得られた行列は、ユニフォームバッファに書き込まれます。resourceUpdates がbeginPass()に渡されている点に注目してください。これは、resourceUpdate()を手動で呼び出す必要がないというショートカットです。

void ExampleRhiWidget::render(QRhiCommandBuffer *cb)
{
    QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
    m_rotation += 1.0f;
    QMatrix4x4 modelViewProjection = m_viewProjection;
    modelViewProjection.rotate(m_rotation, 0, 1, 0);
    resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData());

レンダリングパスでは、3つの頂点を持つ単一のドローコールが記録されます。 初期化ステップで作成されたグラフィックスパイプラインはコマンドバッファにバインドされ、ビューポートはウィジェット全体を覆うように設定されます。(頂点)シェーダーからユニフォームバッファを参照可能にするため、引数なしでsetShaderResources() が呼び出されます。これは、パイプライン作成時にパイプラインに関連付けられていたm_srb を使用することを意味します。 より複雑なレンダラーでは、パイプライン作成時に指定されたものとのlayout-compatible が保たれている限り、別のQRhiShaderResourceBindings オブジェクトを渡すことも珍しくありません。インデックスバッファはなく、頂点バッファのバインディングは1つだけです(vbufBinding 内の単一の要素は、パイプライン作成時に指定されたQRhiVertexInputLayout のバインディングリスト内の単一のエントリを指しています)。

    const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f);
    cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates);

    cb->setGraphicsPipeline(m_pipeline.get());
    const QSize outputSize = renderTarget()->pixelSize();
    cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height()));
    cb->setShaderResources();
    const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
    cb->setVertexInput(0, 1, &vbufBinding);
    cb->draw(3);

    cb->endPass();

レンダリングパスが記録されると、update() が呼び出されます。これは新しいフレームを要求するもので、ウィジェットが継続的に更新され、三角形が回転して見えるようにするために使用されます。 レンダリングスレッド(この場合はメインスレッド)は、デフォルトで表示レートによって制限されます。この例には適切なアニメーションシステムがないため、回転はフレームごとに増加します。つまり、リフレッシュレートが異なるディスプレイでは、三角形の回転速度も異なってしまいます。

    update();
}

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

「 QRhi」 、「Cube RHI Widget Example」、「RHI Window Example」も参照してください 。

© 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.