Beispiel für das RHI-Widget „Cube“

Zeigt, wie man einen texturierten Würfel rendert und ihn mithilfe von „ QPainter “ und Qt Widgets in „ QRhi “ – der Qt 3D-API und Shading-Sprachabstraktionsschicht – integriert.

Anwendung mit komplexen Rendering-Überlagerungen

Screenshot des Beispiels „Cube RHI Widget“

Dieses Beispiel baut auf dem Beispiel „Einfaches RHI-Widget“ auf. Während das einfache Beispiel bewusst minimalistisch und so kompakt wie möglich gehalten ist und lediglich ein einzelnes Dreieck ohne zusätzliche Widgets im Fenster rendert, demonstriert diese Anwendung Folgendes:

  • Verschiedene Widgets im Fenster, von denen einige Daten steuern, die von der Unterklasse „ QRhiWidget “ verwendet werden.
  • Anstatt ständig Aktualisierungen anzufordern, aktualisiert der „ QRhiWidget “ hier den Inhalt seiner zugrunde liegenden Textur nur dann, wenn sich bestimmte zugehörige Daten ändern.
  • Der Würfel wird mit einem „ QRhiTexture “ texturiert, dessen Inhalt von einem „ QImage “ stammt, das ein softwarebasiertes Rendering enthält, das mit „ QPainter “ durchgeführt wird.
  • Der Inhalt des „ QRhiWidget “ „ can be read back “ wird in einer Bilddatei (z. B. einer PNG-Datei) gespeichert.
  • 4-faches Multisample-Antialiasing von can be toggled zur Laufzeit. Die QRhiWidget -Unterklasse ist darauf ausgelegt, die sich ändernde Anzahl der Samples korrekt zu verarbeiten.
  • Das Erzwingen von explicitly specified backing texture size kann dynamisch umgeschaltet und über einen Schieberegler im Bereich von 16×16 bis zu 512×512 Pixel gesteuert werden.
  • Die Unterklasse „ QRhiWidget “ verarbeitet eine sich ändernde „ QRhi “ korrekt. Dies lässt sich in der Praxis beobachten, wenn man das Widget auf oberste Ebene setzt (kein übergeordnetes Element; wird zu einem separaten Fenster) und es anschließend wieder in die Unterelementhierarchie des Hauptfensters einbindet.
  • Vor allem lassen sich einige Widgets – sogar solche mit Halbtransparenz – über dem „ QRhiWidget “ platzieren, was beweist, dass eine korrekte Stapelung und Überblendung möglich ist. Dies ist ein Fall, in dem „ QRhiWidget “ der Einbettung eines nativen Fensters überlegen ist, d. h. einem auf „ QRhi “ basierenden „ QWindow “ unter Verwendung von „ QWidget::createWindowContainer(()“, da es das Stapeln und Beschneiden auf dieselbe Weise ermöglicht wie jedes gewöhnliche, softwaregerenderte QWidget, während die Einbettung eines nativen Fensters je nach Plattform verschiedene Einschränkungen aufweisen kann – so ist es beispielsweise oft schwierig oder ineffizient, zusätzliche Steuerelemente darüber zu platzieren.

Bei der Neuimplementierung von ` initialize()` muss zunächst geprüft werden, ob das zuletzt verwendete ` QRhi ` noch aktuell ist und ob sich die Sample-Anzahl (für Multisample-Antialiasing) geändert hat. Ersteres ist wichtig, da alle Grafikressourcen freigegeben werden müssen, wenn sich die „ QRhi “ ändert, während bei einer sich dynamisch ändernden Sample-Anzahl ein ähnliches Problem speziell bei „ QRhiGraphicsPipeline “-Objekten auftritt, da diese die Sample-Anzahl fest einbinden. Der Einfachheit halber behandelt die Anwendung alle derartigen Änderungen auf dieselbe Weise, indem sie ihre „ scene “-Struktur auf eine standardmäßig konstruierte zurücksetzt, wodurch praktischerweise alle Grafikressourcen freigegeben werden. Anschließend werden alle Ressourcen neu erstellt.

Wenn sich die Größe der zugrunde liegenden Textur (also die Größe des Renderziels) ändert, sind keine besonderen Maßnahmen erforderlich, doch der Einfachheit halber wird ein Signal ausgegeben, damit main() die Position des Overlay-Labels anpassen kann. Der Name der 3D-API wird ebenfalls über ein Signal bereitgestellt, indem QRhi::backendName() abgefragt wird, sobald sich QRhi ändert.

Die Implementierung muss berücksichtigen, dass Multisample-Antialiasing bedeutet, dass ` colorTexture()` gleich ` nullptr` ist, während ` msaaColorBuffer()` gültig ist. Dies ist das Gegenteil davon, wenn MSAA nicht verwendet wird. Der Grund für die Unterscheidung und die Verwendung verschiedener Typen (QRhiTexture, QRhiRenderBuffer) besteht darin, die Verwendung von MSAA mit 3D-Grafik-APIs zu ermöglichen, die zwar keine Unterstützung für Multisample-Texturen bieten, aber Multisample-Renderbuffer unterstützen. Ein Beispiel hierfür ist OpenGL ES 3.0.

Um die aktuelle Pixelgröße und die Anzahl der Samples zu überprüfen, ist eine praktische und kompakte Lösung die Abfrage über ` QRhiRenderTarget`, da man auf diese Weise nicht prüfen muss, welche der beiden Funktionen `colorTexture()` und `msaaColorBuffer()` gültig ist.

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 = {};
    }

Der Rest ist ziemlich selbsterklärend. Die Puffer und Pipelines werden bei Bedarf (neu) erstellt. Der Inhalt der Textur, die zur Texturierung des Würfel-Meshes verwendet wird, wird aktualisiert. Die Szene wird unter Verwendung einer perspektivischen Projektion gerendert. Die Ansicht ist vorerst nur eine einfache Translation.

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

Die Funktion, die das eigentliche Einreihen des Schreibvorgangs in den Uniform-Puffer durchführt, berücksichtigt ebenfalls die vom Benutzer angegebene Rotation und generiert so die endgültige Model-View-Projektionsmatrix.

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

Die Aktualisierung des „ QRhiTexture “, der beim Rendern des Würfels im Fragment-Shader abgetastet wird, ist recht einfach, auch wenn dort viel passiert: Zunächst wird innerhalb eines „ QImage “ eine auf „ QPainter “ basierende Zeichnung generiert. Dabei wird der vom Benutzer bereitgestellte Text verwendet. Anschließend werden die CPU-seitigen Pixeldaten in eine Textur hochgeladen (genauer gesagt wird der Upload-Vorgang in einem „ QRhiResourceUpdateBatch “ aufgezeichnet, der dann später in „render()“ übermittelt wird).

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

Die Initialisierung der Grafikressourcen ist einfach. Es gibt nur einen Vertex-Puffer, keinen Index-Puffer und einen Uniform-Puffer, der lediglich eine 4x4-Matrix enthält (16 Floats).

Die Textur, die die von „ QPainter “ generierte Zeichnung enthält, hat eine Größe von 512x512. Beachten Sie, dass alle Größen (Texturgrößen, Viewports, Scheren, Textur-Upload-Bereiche usw.) bei der Arbeit mit „ QRhi “ immer in Pixeln angegeben werden. Um diese Textur im Shader abzutasten, wird ein „ sampler object “ benötigt (unabhängig davon, dass QRhi-basierte Anwendungen im GLSL-Shader-Code typischerweise kombinierte Bild-Sampler verwenden, die dann bei einigen Shading-Sprachen in separate Textur- und Sampler-Objekte transpiliert werden können, oder bei anderen als kombiniertes Textur-Sampler-Objekt verbleiben, was bedeutet, dass je nach 3D-API zur Laufzeit unter der Haube möglicherweise gar kein natives Sampler-Objekt vorhanden ist – all dies ist für die Anwendung jedoch transparent)

Der Vertex-Shader liest aus dem Uniform-Puffer am Bindungspunkt 0, daher wird „ scene.ubuf “ an dieser Bindungsstelle bereitgestellt. Der Fragment-Shader nimmt eine an Bindungspunkt 1 bereitgestellte Textur ab, daher wird für diese Bindungsstelle ein kombiniertes Textur-Sampler-Paar angegeben.

QRhiGraphicsPipeline ermöglicht Tiefenprüfung/-schreiben und filtert Rückseiten aus. Es stützt sich zudem auf eine Reihe von Standardwerten, z. B. ist die Tiefenvergleichsfunktion standardmäßig auf ` Less` gesetzt, was für uns in Ordnung ist, und der Frontflächenmodus ist auf „gegen den Uhrzeigersinn“ eingestellt, was ebenfalls in Ordnung ist und daher nicht erneut festgelegt werden muss.

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

Bei der Neuimplementierung von ` render()` werden zunächst die vom Benutzer bereitgestellten Daten überprüft. Wenn die Rotation steuernde ` QSlider ` einen neuen Wert bereitgestellt hat oder die ` QTextEdit ` mit dem Würfeltext ihren Text geändert hat, werden die Grafikressourcen, deren Inhalt von diesen Daten abhängt, aktualisiert.

Anschließend wird ein einzelner Renderdurchlauf mit einem einzigen Draw-Call aufgezeichnet. Die Würfel-Mesh-Daten werden in einem nicht-verschachtelten Format bereitgestellt, weshalb zwei Vertex-Input-Bindings erforderlich sind: eines für die Positionen (x, y, z) und das andere für die UVs (u, v), mit einem Start-Offset, der 36 x-y-z-Float-Paaren entspricht.

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

Wie werden die vom Benutzer bereitgestellten Daten gesendet? Nehmen wir zum Beispiel die Rotation. main() ist mit dem Signal „ valueChanged “ des „ QSlider “ verbunden. Wenn dieses Signal ausgelöst wird, ruft das verbundene Lambda-Ausdrucks-Objekt setCubeRotation() auf dem „ExampleRhiWidget“ auf. Wenn der Wert hier von dem vorherigen abweicht, wird er gespeichert und ein „Dirty“-Flag gesetzt. Anschließend – und das ist besonders wichtig – wird ` update()` für das `ExampleRhiWidget` aufgerufen. Dadurch wird das Rendern eines neuen Frames in die Backing-Textur des „ QRhiWidget “ ausgelöst. Ohne diesen Schritt würde sich der Inhalt des `ExampleRhiWidget` beim Ziehen des Schiebereglers nicht aktualisieren.

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

Beispielprojekt auf code.qt.io

Siehe auch „ QRhi“ , „Einfaches RHI-Widget-Beispiel“ und „RHI-Fenster-Beispiel“.

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