Würfel RHI Widget Beispiel

Zeigt, wie ein texturierter Würfel gerendert und mit QPainter und Widgets integriert wird, unter Verwendung der 3D-API von QRhi Qt und der Abstraktionsschicht der Schattierungssprache.

Screenshot des Cube RHI Widget Beispiels

Dieses Beispiel baut auf dem einfachen RHI Widget-Beispiel auf. Während das einfache Beispiel absichtlich minimal und so kompakt wie möglich ist und nur ein einziges Dreieck ohne zusätzliche Widgets im Fenster darstellt, demonstriert diese Anwendung:

  • Verschiedene Widgets im Fenster, von denen einige Daten steuern, die von der Unterklasse QRhiWidget verbraucht werden.
  • Anstatt ständig Aktualisierungen anzufordern, aktualisiert die QRhiWidget hier den Inhalt in ihrer Hintergrundtextur nur, wenn sich einige zugehörige Daten ändern.
  • Der Würfel wird mit einer QRhiTexture texturiert, die ihren Inhalt von einer QImage bezieht, die softwarebasiertes Rendering mit QPainter enthält.
  • Der Inhalt der QRhiWidget can be read back und wird in einer Bilddatei (z. B. einer PNG-Datei) gespeichert.
  • 4x Multisample-Antialiasing can be toggled zur Laufzeit. Die Unterklasse QRhiWidget ist darauf vorbereitet, mit der sich ändernden Sampleanzahl korrekt umzugehen.
  • Das Erzwingen einer explizit spezifizierten Hintergrundtexturgröße kann dynamisch umgeschaltet und mit einem Schieberegler zwischen 16x16 und 512x512 Pixeln gesteuert werden.
  • Die Unterklasse QRhiWidget geht korrekt mit einer sich ändernden QRhi um. Dies kann man in Aktion sehen, wenn man das Widget zur obersten Ebene macht (kein Parent; wird ein separates Fenster) und es dann wieder in die Child-Hierarchie des Hauptfensters reparentiert.
  • Am wichtigsten ist, dass einige Widgets, sogar mit Halbtransparenz, auf QRhiWidget platziert werden können, was beweist, dass korrektes Stapeln und Überblenden möglich ist. Dies ist ein Fall, in dem QRhiWidget der Einbettung eines nativen Fensters, d. h. eines QRhi-basierten QWindow unter Verwendung von QWidget::createWindowContainer(), überlegen ist, weil es das Stapeln und Beschneiden auf die gleiche Weise wie jedes gewöhnliche, software-gerenderte QWidget ermöglicht, während die Einbettung eines nativen Fensters je nach Plattform verschiedene Einschränkungen haben kann, z. B. kann es oft schwierig oder ineffizient sein, zusätzliche Steuerelemente oben zu platzieren.

Bei der Neuimplementierung von initialize() muss als erstes geprüft werden, ob die QRhi, mit der wir zuletzt gearbeitet haben, noch aktuell ist und ob sich die Anzahl der Samples (für Multisample-Antialiasing) geändert hat. Ersteres ist wichtig, weil alle Grafikressourcen freigegeben werden müssen, wenn sich die QRhi ändert, während bei einer sich dynamisch ändernden Abtastanzahl ein ähnliches Problem speziell für QRhiGraphicsPipeline Objekte auftritt, wie die, die die Abtastanzahl einbacken. Der Einfachheit halber behandelt die Anwendung alle derartigen Änderungen auf die gleiche Weise, indem sie ihre scene struct auf eine standardmäßig erstellte Struktur zurücksetzt, die bequemerweise alle Grafikressourcen freigibt. Alle Ressourcen werden dann neu erstellt.

Wenn sich die Größe der Hintergrundtextur (also die Größe des Rendering-Ziels) ändert, ist keine besondere Aktion erforderlich, aber es wird der Einfachheit halber ein Signal ausgegeben, so dass main() das Overlay-Label neu positionieren kann. Der 3D-API-Name wird ebenfalls über ein Signal ausgegeben, indem QRhi::backendName() abgefragt wird, sobald sich die QRhi ändert.

Die Implementierung muss sich bewusst sein, dass Multisample Antialiasing bedeutet, dass colorTexture() nullptr ist, während msaaColorBuffer() gültig ist. Dies ist das Gegenteil von MSAA, wenn es nicht verwendet wird. Der Grund für die Unterscheidung und die Verwendung verschiedener Typen (QRhiTexture, QRhiRenderBuffer) ist, dass MSAA mit 3D-Grafik-APIs verwendet werden kann, die keine Unterstützung für Multisample-Texturen, aber Unterstützung für Multisample-Renderbuffer haben. Ein Beispiel hierfür ist OpenGL ES 3.0.

Bei der Überprüfung der aktuellen Pixelgröße und Sampleanzahl ist eine bequeme und kompakte Lösung die Abfrage über die QRhiRenderTarget, da man auf diese Weise nicht überprüfen muss, welche der Funktionen colorTexture() und msaaColorBuffer() gültig sind.

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 recht selbsterklärend. Die Puffer und Pipelines werden, falls erforderlich, (neu) erstellt. Der Inhalt der Textur, die zur Texturierung des Würfelnetzes verwendet wird, wird aktualisiert. Die Szene wird unter Verwendung einer perspektivischen Projektion gerendert. Die Ansicht ist im Moment nur eine einfache Übersetzung.

    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 Schreibens des einheitlichen Puffers durchführt, berücksichtigt auch die vom Benutzer angegebene Drehung und erzeugt so die endgültige Modellansichtsprojektionsmatrix.

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 der QRhiTexture, die beim Rendering des Würfels im Fragment-Shader abgetastet wird, ist recht einfach, auch wenn dort eine Menge passiert: Zunächst wird eine QPainter-basierte Zeichnung innerhalb einer QImage erzeugt. Dann werden die CPU-seitigen Pixeldaten in eine Textur hochgeladen (genauer gesagt wird der Upload-Vorgang in einer QRhiResourceUpdateBatch aufgezeichnet, die dann später in render() übergeben 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 einheitlichen Puffer, der nur eine 4x4-Matrix enthält (16 Floats).

Die Textur, die die QPainter-generierte Zeichnung enthält, hat eine Größe von 512x512. Beachten Sie, dass alle Größen (Texturgrößen, Ansichtsfenster, Scheren, Textur-Upload-Regionen usw.) immer in Pixel angegeben werden, wenn Sie mit QRhi arbeiten. Um diese Textur im Shader zu sampeln, wird eine sampler object benötigt (ungeachtet der Tatsache, dass QRhi-basierte Anwendungen typischerweise kombinierte Bild-Sampler im GLSL-Shader-Code verwenden, die dann in einigen Shading-Sprachen in separate Textur- und Sampler-Objekte transpiliert werden oder in anderen Sprachen ein kombiniertes Textur-Sampler-Objekt bleiben können, was bedeutet, dass es zur Laufzeit je nach 3D-API kein natives Sampler-Objekt unter der Haube gibt, aber das ist alles für die Anwendung transparent)

Der Vertex-Shader liest aus dem einheitlichen Puffer am Bindungspunkt 0, daher ist scene.ubuf an diesem Bindungspunkt ausgesetzt. Der Fragment-Shader sampelt eine Textur, die am Bindungspunkt 1 bereitgestellt wird, daher wird ein kombiniertes Textur-Sampler-Paar für diesen Bindungspunkt angegeben.

Die Website QRhiGraphicsPipeline ermöglicht die Tiefenprüfung und -schreibefunktion und entfernt Rückseiten. Es verlässt sich auch auf eine Reihe von Standardwerten, z. B. ist die Tiefenvergleichsfunktion standardmäßig auf Less eingestellt, was für uns in Ordnung ist, und der Modus für die Vorderseite ist gegen den Uhrzeigersinn, was auch gut ist, so dass es nicht erneut eingestellt 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 QSlider, die die Drehung steuert, einen neuen Wert angegeben 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 Rendering-Durchgang mit einem einzigen Zeichenaufruf aufgezeichnet. Die Daten des Würfelnetzes werden in einem nicht verschachtelten Format bereitgestellt, daher sind zwei Vertex-Eingabebindungen erforderlich, eine für die Positionen (x, y, z), die andere für die UVs (u, v), mit einem Startoffset, 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 zur Verfügung gestellten Daten gesendet? Nehmen wir zum Beispiel die Rotation. main() verbindet sich mit dem Signal QSlider's valueChanged. Wenn es ausgesendet wird, ruft das verbundene Lamda setCubeRotation() auf dem ExampleRhiWidget auf. Ist der Wert anders als zuvor, wird er gespeichert und ein Dirty-Flag gesetzt. Dann wird, was am wichtigsten ist, update() auf dem ExampleRhiWidget aufgerufen. Dadurch wird das Rendern eines neuen Frames in die Hintergrundtextur von QRhiWidget ausgelöst. Ohne diese Funktion würde sich der Inhalt des ExampleRhiWidget nicht aktualisieren, wenn der Schieberegler gezogen wird.

    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 @ code.qt.io

Siehe auch QRhi, Simple RHI Widget Example und RHI Window Example.

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