キューブ RHI ウィジェットの例

QRhi Qt の 3D API とシェーディング言語抽象化レイヤを使用して、テクスチャ付きキューブをレンダリングし、QPainter とウィジェットに統合する方法を示します。

キューブ RHI ウィジェット例のスクリーンショット

この例は、シンプルなRHIウィジェットの例に基づいています。シンプルな例では、ウィンドウ内にウィジェットを追加することなく、単一の三角形のみをレンダリングし、意図的に最小化し、可能な限りコンパクトにしていますが、このアプリケーションではそれを実証しています:

  • ウィンドウ内にさまざまなウィジェットがあり、そのうちのいくつかは、QRhiWidget サブクラスによって消費されるデータを制御しています。
  • 更新を継続的に要求する代わりに、QRhiWidget は、関連するデータが変更された場合にのみ、そのバッキング・テクスチャのコンテンツを更新します。
  • キューブのテクスチャは、QPainter で実行されるソフトウェアベースのレンダリングを含むQImage からコンテンツを取得するQRhiTexture を使用しています。
  • QRhiWidget can be read back のコンテンツは、画像ファイル(PNGファイルなど)に保存されます。
  • 4x マルチサンプル アンチエイリアシングcan be toggled を実行時に行います。QRhiWidget サブクラスは、変化するサンプル数を正しく処理できるように準備されています。
  • 明示的に指定されたバッキング・テクスチャ・サイズを強制的に切り替えることができ、16x16から512x512ピクセルまでの間のスライダーで動的に制御できます。
  • QRhiWidget サブクラスは、変化するQRhi を正しく処理します。これは、ウィジェットをトップレベル(親がない;別ウィンドウになる)にしてから、メインウィンドウの子階層に再ペアレントするときに、実際に見ることができます。
  • 最も重要なことは、いくつかのウィジェットは、半透明でさえ、QRhiWidget の上に置くことができ、正しいスタックとブレンドが実現可能であることを証明していることである。これは、QRhiWidget がネイティブウィンドウの埋め込み、つまりQWidget::createWindowContainer()を使ったQRhi-basedQWindow より優れているケースです。なぜなら、QWidget は、通常のソフトウェアレンダリングされたウィンドウと同じようにスタックとクリッピングができるからです。

initialize() の再実装では、最初に行うべきことは、最後に扱ったQRhi がまだ最新かどうか、およびサンプル数(マルチサンプル・アンチエイリアシング用)が変更されていないかどうかをチェックすることです。前者が重要なのは、QRhi が変更されたときに、すべてのグラフィックス・リソースを解放しなければならないからです。一方、サンプル・カウントが動的に変更されると、サンプル・カウントを焼き込むものとして、QRhiGraphicsPipeline オブジェクトに特有の同様の問題が発生します。簡単にするために、アプリケーションはこのような変更をすべて同じ方法で処理します。scene 構造体をデフォルトで構築されたものにリセットすることで、都合よくすべてのグラフィックス・リソースをドロップします。その後、すべてのリソースが再作成されます。

バッキング・テクスチャ・サイズ(つまりレンダー・ターゲット・サイズ)が変更された場合、特別なアクションは必要ありませんが、main()がオーバーレイ・ラベルを再配置できるように、便宜上シグナルが発行されます。QRhi が変更されるたびにQRhi::backendName() に問い合わせることで、3D API 名もシグナルを通じて公開される。

実装は、マルチサンプル・アンチエイリアシングはcolorTexture() がnullptr であることを意味するが、msaaColorBuffer() は有効であることを認識しなければならない。これは、MSAAが使用されていない場合とは逆である。異なるタイプ(QRhiTextureQRhiRenderBuffer )を区別して使用する理由は、マルチサンプルテクスチャのサポートはないが、マルチサンプルレンダーバッファのサポートはある3DグラフィックスAPIでMSAAを使用できるようにするためです。この例はOpenGL ES 3.0です。

最新のピクセルサイズとサンプル数をチェックする場合、便利でコンパクトな解決策は、QRhiRenderTarget を介してクエリすることです。この方法では、colorTexture() と msaaColorBuffer() のどれが有効かをチェックする必要がないからです。

void ExampleRhiWidget::initialize(QRhiCommandBuffer *)
{
    if (m_rhi != rhi()) {
        m_rhi = rhi();
        scene = {};
        emit rhiChanged(QString::fromUtf8(m_rhi->backendName()));
    }
    if (m_pixelSize != renderTarget()->pixelSize()) {
        m_pixelSize = renderTarget()->pixelSize();
        emit resized();
    }
    if (m_sampleCount != renderTarget()->sampleCount()) {
        m_sampleCount = renderTarget()->sampleCount();
        scene = {};
    }

あとは自明です。必要に応じて、バッファとパイプラインが(再)作成されます。キューブメッシュのテクスチャに使用されるテクスチャの内容が更新されます。シーンは透視投影を使ってレンダリングされます。ビューは今のところ単純な変換だけです。

    if (!scene.vbuf) {
        initScene();
        updateCubeTexture();
    }

    scene.mvp = m_rhi->clipSpaceCorrMatrix();
    scene.mvp.perspective(45.0f, m_pixelSize.width() / (float) m_pixelSize.height(), 0.01f, 1000.0f);
    scene.mvp.translate(0, 0, -4);
    updateMvp();
}

ユニフォームバッファの書き込みを実際にエンキューする関数は、ユーザが提供した回転も考慮し、最終的なモデルビュー-投影行列を生成します。

void ExampleRhiWidget::updateMvp()
{
    QMatrix4x4 mvp = scene.mvp * QMatrix4x4(QQuaternion::fromEulerAngles(QVector3D(30, itemData.cubeRotation, 0)).toRotationMatrix());
    if (!scene.resourceUpdates)
        scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
    scene.resourceUpdates->updateDynamicBuffer(scene.ubuf.get(), 0, 64, mvp.constData());
}

QPainter QImage立方体をレンダリングするときにフラグメントシェーダでサンプリングされるQRhiTexture の更新は、その中で多くのことが起こっているとはいえ、非常に単純です。次に、CPU 側のピクセルデータがテクスチャにアップロードされます(より正確には、アップロード操作がQRhiResourceUpdateBatch に記録され、後で render() でサブミットされます)。

void ExampleRhiWidget::updateCubeTexture()
{
    QImage image(CUBE_TEX_SIZE, QImage::Format_RGBA8888);
    const QRect r(QPoint(0, 0), CUBE_TEX_SIZE);
    QPainter p(&image);
    p.fillRect(r, QGradient::DeepBlue);
    QFont font;
    font.setPointSize(24);
    p.setFont(font);
    p.drawText(r, itemData.cubeText);
    p.end();

    if (!scene.resourceUpdates)
        scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
    scene.resourceUpdates->uploadTexture(scene.cubeTex.get(), image);
}

グラフィックリソースの初期化は単純です。頂点バッファのみで、インデックスバッファはなく、4x4 の行列(16 個の浮動小数点)のみを含む均一バッファがあります。

QPainter-生成された描画を含むテクスチャのサイズは512x512です。テクスチャのサイズ、ビューポート、ハサミ、テクスチャのアップロード 領域など)はすべて、QRhi を扱うときは常にピクセルで あることに注意してください。シェーダでこのテクスチャをサンプリングするには、sampler object が必要です(QRhi ベースのアプリケーションは、通常、GLSL シェーダコードで複合イメージサンプラを使用し、その後、いくつかのシェーディング言語でテクスチャとサンプラオブジェクトに分離してトランスパイルされるか、他の言語でテクスチャとサンプラの複合オブジェクトにとどまるかもしれないという事実とは無関係に、3D API によって、実行時にボンネットの下にネイティブサンプラオブジェクトが実際には存在しないかもしれませんが、これはすべてアプリケーションには透過的です)。

頂点シェーダはユニフォームバッファからバインディングポイント 0 で読み込むため、scene.ubuf はそのバインディング位置で公開されます。フラグメントシェーダは、バインディングポイント 1 で提供されるテクスチャをサンプリングするため、そのバインディング位置ではテクスチャとサンプラの組み合わせが指定されます。

QRhiGraphicsPipeline は、深度テスト/書き込みを有効にし、バックフェースをカ ルします。また、多くのデフォルトに依存しています。たとえば、深度比較関数のデフォルトはLess で、これは私たちにとって問題ありません。また、フロントフェイスモードは反時計回りですが、これもそのままで問題ないため、再度設定する必要はありません。

    scene.vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube)));
    scene.vbuf->create();

    scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
    scene.resourceUpdates->uploadStaticBuffer(scene.vbuf.get(), cube);

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

    scene.cubeTex.reset(m_rhi->newTexture(QRhiTexture::RGBA8, CUBE_TEX_SIZE));
    scene.cubeTex->create();

    scene.sampler.reset(m_rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
                                               QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
    scene.sampler->create();

    scene.srb.reset(m_rhi->newShaderResourceBindings());
    scene.srb->setBindings({
        QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, scene.ubuf.get()),
        QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, scene.cubeTex.get(), scene.sampler.get())
    });
    scene.srb->create();

    scene.ps.reset(m_rhi->newGraphicsPipeline());
    scene.ps->setDepthTest(true);
    scene.ps->setDepthWrite(true);
    scene.ps->setCullMode(QRhiGraphicsPipeline::Back);
    scene.ps->setShaderStages({
        { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shader_assets/texture.vert.qsb")) },
        { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/texture.frag.qsb")) }
    });
    QRhiVertexInputLayout inputLayout;
    // The cube is provided as non-interleaved sets of positions, UVs, normals.
    // Normals are not interesting here, only need the positions and UVs.
    inputLayout.setBindings({
        { 3 * sizeof(float) },
        { 2 * sizeof(float) }
    });
    inputLayout.setAttributes({
        { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
        { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
    });
    scene.ps->setSampleCount(m_sampleCount);
    scene.ps->setVertexInputLayout(inputLayout);
    scene.ps->setShaderResourceBindings(scene.srb.get());
    scene.ps->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
    scene.ps->create();

render() の再実装では、まず、ユーザーが提供したデータがチェックされる。回転を制御するQSlider が新しい値を提供した場合、または立方体のテキストを持つQTextEdit がそのテキストを変更した場合、そのようなデータに依存するグラフィックリソースの内容が更新されます。

その後、1 回の描画呼び出しによる 1 回のレンダーパスが記録されます。1つは位置(x, y, z)、もう1つはUV(u, v)で、36個のx-y-z floatペアに対応する開始オフセットがあります。

void ExampleRhiWidget::render(QRhiCommandBuffer *cb)
{
    if (itemData.cubeRotationDirty) {
        itemData.cubeRotationDirty = false;
        updateMvp();
    }

    if (itemData.cubeTextDirty) {
        itemData.cubeTextDirty = false;
        updateCubeTexture();
    }

    QRhiResourceUpdateBatch *resourceUpdates = scene.resourceUpdates;
    if (resourceUpdates)
        scene.resourceUpdates = nullptr;

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

    cb->setGraphicsPipeline(scene.ps.get());
    cb->setViewport(QRhiViewport(0, 0, m_pixelSize.width(), m_pixelSize.height()));
    cb->setShaderResources();
    const QRhiCommandBuffer::VertexInput vbufBindings[] = {
        { scene.vbuf.get(), 0 },
        { scene.vbuf.get(), quint32(36 * 3 * sizeof(float)) }
    };
    cb->setVertexInput(0, 2, vbufBindings);
    cb->draw(36);

    cb->endPass();
}

ユーザーが提供したデータはどのように送られるのでしょうか?回転を例にとると、main() はQSlidervalueChanged シグナルに接続します。発信されると、接続されたラムダはExampleRhiWidgetのsetCubeRotation()を呼び出します。ここで、値が以前と異なる場合、それが格納され、ダーティ・フラグが設定されます。そして、最も重要なのは、ExampleRhiWidget上でupdate ()が呼び出されることです。これは、QRhiWidget'のバッキングテクスチャに新しいフレームをレンダリングするトリガーです。これがないと、スライダーをドラッグしてもExampleRhiWidgetの内容は更新されません。

    void setCubeTextureText(const QString &s)
    {
        if (itemData.cubeText == s)
            return;
        itemData.cubeText = s;
        itemData.cubeTextDirty = true;
        update();
    }

    void setCubeRotation(float r)
    {
        if (itemData.cubeRotation == r)
            return;
        itemData.cubeRotation = r;
        itemData.cubeRotationDirty = true;
        update();
    }

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

QRhiSimple 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.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。