Cube RHI ウィジェットの例
Qtの3D APIおよびシェーディング言語抽象化レイヤーであるQRhi (RHI)を使用して、テクスチャ付きキューブを描画し、QPainter およびウィジェットと統合する方法を示します。

Cube RHI ウィジェットの例のスクリーンショット
このサンプルは、「Simple RHI ウィジェットサンプル」を基にしています。シンプルなサンプルは意図的に最小限かつ可能な限りコンパクトに構成されており、ウィンドウ内に追加のウィジェットを配置せず単一の三角形のみをレンダリングしていますが、このアプリケーションでは以下の点を実演しています:
- ウィンドウ内に様々なウィジェットが存在し、その一部は「QRhiWidget 」のサブクラスが利用するデータを制御していること。
- QRhiWidget は、継続的に更新を要求するのではなく、関連するデータが変更された場合にのみ、バッキングテクスチャ内のコンテンツを更新します。
- この立方体のテクスチャは、`QPainter` を使用してソフトウェアベースのレンダリングを行う `QImage ` からコンテンツを取得する `QRhiTexture ` によって適用されています。
- QRhiWidget のcan be read back の内容は、画像ファイル(PNGファイルなど)に保存されます。
- 実行時に 4 倍マルチサンプルアンチエイリアシングのcan be toggled を実行します。QRhiWidget サブクラスは、サンプル数の変化に正しく対応できるよう準備されています。
- explicitly specified backing texture size の強制適用は動的に切り替え可能で、スライダーを使用して16x16から最大512x512ピクセルまで制御できます。
- QRhiWidget サブクラスは、変化するQRhi を正しく処理します。これは、ウィジェットをトップレベル(親なし、独立したウィンドウとなる)にしてから、再びメインウィンドウの子階層に再配置した際に、その動作を確認できます。
- 最も重要な点は、半透明のウィジェットであっても、QRhiWidget の上に配置できることであり、これにより正しい重ね合わせとブレンドが可能であることが証明されます。これは、QRhiWidget が、ネイティブウィンドウ(つまり、QWidget::createWindowContainer() を使用したQRhi ベースのQWindow )の埋め込みよりも優れているケースです。 。なぜなら、 は通常のソフトウェアレンダリングされたQWidget と同様にスタックやクリッピングが可能であるのに対し、ネイティブウィンドウの埋め込みはプラットフォームによっては様々な制限があり、例えば、その上に追加のコントロールを配置することが困難だったり非効率だったりすることが多いからです。
initialize()の再実装において、まず最初に行うべきことは、前回操作したQRhi が依然として最新の状態であるか、およびサンプル数(マルチサンプルアンチエイリアシング用)に変更がないかを確認することです。 前者が重要なのは、QRhi が変更されるとすべてのグラフィックスリソースを解放しなければならないのに対し、サンプル数が動的に変化する場合、特にQRhiGraphicsPipeline オブジェクトに関しては、サンプル数が内部に固定されているため同様の問題が生じるからです。簡便さのため、アプリケーションはこのような変更をすべて同じ方法で処理します。つまり、scene 構造体をデフォルト構築されたものにリセットすることで、これによりすべてのグラフィックスリソースが適切に解放されます。 その後、すべてのリソースが再作成されます。
バッキングテクスチャのサイズ(つまり、レンダリングターゲットのサイズ)が変更されても、特別な処置は必要ありませんが、便宜上、main() がオーバーレイラベルの位置を再配置できるように、シグナルが発信されます。また、QRhi が変更されるたびにQRhi::backendName() を呼び出すことで、3D API 名もシグナルを介して公開されます。
実装では、マルチサンプルアンチエイリアシング(MSAA)が有効な場合、colorTexture() の値がnullptr となる一方、msaaColorBuffer() も有効であることに留意する必要があります。これは、MSAA を使用していない場合とは逆の挙動です。 区別して異なる型(QRhiTexture 、QRhiRenderBuffer )を使用する理由は、マルチサンプルテクスチャのサポートはないが、マルチサンプルレンダーバッファのサポートがある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());
}キューブをレンダリングする際にフラグメントシェーダーでサンプリングされるQRhiTexture の更新は、内部では多くの処理が行われているにもかかわらず、非常に単純です。まず、QImage 内でQPainter に基づく描画が生成されます。これにはユーザーが指定したテキストが使用されます。 次に、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 のテキストが変更されていたりする場合、それらのデータに依存するグラフィックスリソースが更新されます。
その後、単一のドローコールによる単一のレンダリングパスが記録されます。 キューブメッシュデータは非インターリーブ形式で提供されるため、2 つの頂点入力バインディングが必要となります。1 つは位置 (x, y, z)、もう 1 つは UV (u, v) で、36 組の x-y-z 浮動小数点ペアに対応する開始オフセットが設定されます。
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() は、QSlider のvalueChanged シグナルに接続します。このシグナルがエミットされると、接続されたラムダ関数が 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();
}「 QRhi 」、「Simple 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.