シンプルな RHI ウィジェットの例

Qt の 3D API とシェーディング言語の抽象化レイヤであるQRhi を使って三角形をレンダリングする方法を示します。

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

この例は、QWidget の世界におけるRHI Window Exampleと多くの点で対応しています。このアプリケーションのQRhiWidget サブクラスは、基本的な頂点シェーダとフラグメントシェーダを備えたシンプルなグラフィックスパイプラインを使用して、単一の三角形をレンダリングします。QWidget プレーンなQWindow ベースのアプリケーションとは異なり、この例では、ウィンドウとQRhi のセットアップや、スワップチェーンとウィンドウイベントの処理など、低レベルの詳細について心配する必要はありません。QRhiWidget のサブクラスのインスタンスは、QVBoxLayout に追加されます。 この例を最小でコンパクトに保つために、ウィジェットや3Dコンテンツは追加されません。

QRhiWidget のサブクラスであるExampleRhiWidget のインスタンスがトップレベル ウィジェットの子階層に追加されると、対応するウィンドウは自動的に Direct 3D、Vulkan、Metal、または OpenGL でレンダリングされたウィンドウになります。QPainter でレンダリングされたウィジェットのコンテンツ、つまりQRhiWidgetQOpenGLWidget 、または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 -render のシンプルなペアで、重要な変更時にinitialize が再呼び出される方が、これに適している。

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 Resource System を介して、実行ファイルに埋め込まれた.qsb ファイルを事前条件付きで出荷します。モジュールの依存関係のため(そしてqmakeをまだサポートしているため)、この例では便利なCMake関数qt_add_shaders() を使用せず、ソースツリーの一部として.qsb ファイルを同梱しています。実際のアプリケーションでは、これを避け、むしろQt Shader 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() に問い合わせると、常に最新のピクセル単位のサイズが得られるので、特別な処理は必要ありません。この例では、デフォルト(RGBA8とマルチサンプル・アンチエイリアスなし)しか使用しないため、テクスチャ・フォーマットとmultisample settings

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 フロートです(xy をスキップ)。各グラフィックス・パイプラインは、QRhiRenderPassDescriptor に関連付けられている必要があります。これは、ベース・クラスが管理するQRhiRenderTarget から取得できます。

注意: この例では、QRhiWidget のデフォルトであるautoRenderTargettrue に設定しています。そのため、レンダーターゲットを管理する必要はなく、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 の違いに対応するため、投影行列にはQRhicorrection matrix が含まれます。(例えば、YダウンとYアップ)

z の値が 0 の三角形が見えるように、-4 の変換が適用されます。

    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);
}

レンダリング

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

初期化ステップで計算されたビュー投影行列は、モデル行列と結合されます。その結果、行列がユニフォームバッファに書き込まれます。resourceUpdatesbeginPass() に渡されることに注意してください。これは、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 オブジェクトを渡すことも珍しくありません。インデックス バッファはなく、単一の頂点バッファ バインディングがあります(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

QRhiCube RHI Widget ExampleRHI Window Exampleも参照してください。

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