En esta página

Ejemplo de widget RHI simple

Muestra cómo renderizar un triángulo utilizando QRhi, la API 3D de Qt y la capa de abstracción del lenguaje de sombreado.

Triángulo de color sobre fondo verde

Captura de pantalla del ejemplo Simple RHI Widget

Este ejemplo es, en muchos sentidos, la contrapartida del Ejemplo de Ventana RHI en el mundo QWidget. La subclase QRhiWidget de esta aplicación renderiza un único triángulo, utilizando un canal gráfico simple con sombreadores básicos de vértices y fragmentos. A diferencia de la aplicación basada en QWindow, este ejemplo no necesita preocuparse de detalles de bajo nivel, como la configuración de la ventana y QRhi, o el manejo de la cadena de intercambio y los eventos de ventana, ya que de eso se encarga el framework QWidget. La instancia de la subclase QRhiWidget se añade a un QVBoxLayout. Para mantener el ejemplo mínimo y compacto, no se introducen más widgets ni contenido 3D.

Una vez que una instancia de ExampleRhiWidget, una subclase de QRhiWidget, se añade a la jerarquía de un widget de nivel superior, la ventana correspondiente se convierte automáticamente en una ventana renderizada en Direct 3D, Vulkan, Metal u OpenGL. El contenido del widget renderizado en QPainter, es decir, todo lo que no es QRhiWidget, QOpenGLWidget, o QQuickWidget, se carga en una textura, mientras que los widgets especiales mencionados se renderizan en una textura cada uno. El conjunto resultante de textures es compuesto por el backingstore del widget de nivel superior.

Estructura y main()

La función main() es bastante simple. El widget de nivel superior tiene por defecto un tamaño de 720p (este tamaño está en unidades lógicas, el tamaño real del píxel puede ser diferente, dependiendo del scale factor. La ventana es redimensionable. QRhiWidget hace que sea sencillo implementar subclases que traten correctamente el redimensionamiento del widget debido al tamaño de la ventana o a cambios en el diseño.

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

La subclase QRhiWidget reimplementa las dos virtuales: initialize() y render(). initialize() es llamada al menos una vez antes de render(), pero también es invocada en una serie de cambios importantes, como cuando la textura de fondo del widget es recreada debido a un cambio en el tamaño del widget, cuando cambian los parámetros del objetivo de renderizado, o cuando el widget cambia a un nuevo QRhi debido a que se mueve a una nueva ventana de nivel superior.

Nota: A diferencia del modelo initializeGL - resizeGL - paintGL heredado de QOpenGLWidget, en QRhiWidget sólo hay dos virtuales. Esto se debe a que hay más eventos especiales de los que es posible que haya que ocuparse aparte del cambio de tamaño, por ejemplo, cuando se reparenting a una ventana de nivel superior diferente. (Las implementaciones robustas de QOpenGLWidget tenían que lidiar con esto realizando una contabilidad adicional, por ejemplo, haciendo un seguimiento del tiempo de vida asociado a QOpenGLContext, lo que significa que los tres virtuales no eran suficientes). Un par más simple de initialize - render, donde initialize se reinvoca ante cambios importantes es más adecuado para esto.

La instancia QRhi no es propiedad del widget. Va a ser consultada en initialize() from the base class . Almacenarlo como un miembro permite reconocer los cambios cuando initialize() es invocado de nuevo. Los recursos gráficos, como los buffers de vértices y uniformes, o el pipeline gráfico están sin embargo bajo el control de 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;
};

Para que la sentencia #include <rhi/qrhi.h> funcione, la aplicación debe enlazarse a GuiPrivate (o gui-private con qmake). Consulta QRhi para más detalles sobre la promesa de compatibilidad de la familia de APIs QRhi.

CMakeLists.txt

target_link_libraries(simplerhiwidget PRIVATE
    Qt6::Core
    Qt6::Gui
    Qt6::GuiPrivate
    Qt6::Widgets
)

Configuración de renderizado

En examplewidget.cpp la implementación del widget utiliza una función de ayuda para cargar un objeto QShader desde un archivo .qsb. Esta aplicación envía archivos .qsb preconfigurados incrustados en el ejecutable a través del sistema de recursos de Qt. Debido a las dependencias de los módulos (y debido a que aún soporta qmake), este ejemplo no utiliza la conveniente función de CMake qt_add_shaders(), sino que viene con los archivos .qsb como parte del árbol de fuentes. Se recomienda a las aplicaciones del mundo real que eviten esto y utilicen las características de integración CMake del módulo Qt Shader Tools (qt_add_shaders). Independientemente del enfoque, en el código C++ la carga de los archivos .qsb empaquetados/generados es la misma.

static QShader getShader(const QString &name)
{
    QFile f(name);
    return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
}

Veamos la implementación de initialize(). En primer lugar, el objeto QRhi se consulta y almacena para su uso posterior, y también para permitir la comparación en futuras invocaciones de la función. Cuando hay un desajuste (por ejemplo, cuando el widget se mueve entre ventanas), la recreación de los recursos gráficos que necesitan ser recreados se activa destruyendo y anulando un objeto adecuado, en este caso el m_pipeline. El ejemplo no demuestra activamente el reparto entre ventanas, pero está preparado para manejarlo. También está preparado para manejar el cambio de tamaño del widget que puede ocurrir al redimensionar la ventana. Esto no necesita un manejo especial ya que initialize() es invocado cada vez que esto ocurre, y por lo tanto la consulta a renderTarget()->pixelSize() o colorTexture()->pixelSize() siempre da el último tamaño actualizado en píxeles. Para lo que este ejemplo no está preparado es para cambiar color buffer formats y multisample settings ya que sólo utiliza los valores por defecto (RGBA8 y sin antialiasing multimuestra).

void ExampleRhiWidget::initialize(QRhiCommandBuffer *cb)
{
    if (m_rhi != rhi()) {
        m_pipeline.reset();
        m_rhi = rhi();
    }

Cuando los recursos gráficos necesitan ser (re)creados, initialize() lo hace usando el típico código basado en QRhi. Un único búfer de vértices con la posición intercalada - datos de vértices de color es suficiente, mientras que la matriz de proyección modelview se expone a través de un búfer uniforme de 64 bytes (16 floats). El búfer uniforme es el único recurso visible del shader, y sólo se utiliza en el shader de vértices. El conducto de gráficos se basa en muchos valores predeterminados (por ejemplo, prueba de profundidad desactivada, mezcla desactivada, escritura de color activada, eliminación de caras desactivada, topología de triángulos predeterminada, etc.). La disposición de los datos de vértice es x, y, r, g, b, por lo que el stride es de 5 floats, mientras que el segundo atributo de entrada de vértice (el color) tiene un offset de 2 floats (omitiendo x y y). Cada canalización gráfica tiene que estar asociada a un QRhiRenderPassDescriptor. Este puede ser recuperado del QRhiRenderTarget gestionado por la clase base.

Nota: Este ejemplo se basa en el QRhiWidget's por defecto de autoRenderTarget ajustado a true. Es por eso que no necesita gestionar el objetivo de renderizado, pero puede simplemente consultar el existente llamando a 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);
    }

Por último, se calcula la matriz de proyección. Esto depende del tamaño del widget y por lo tanto se hace incondicionalmente en cada invocación de las funciones.

Nota: Cualquier cálculo de tamaño y ventana gráfica sólo debe basarse en el tamaño de píxel consultado desde el recurso que sirve como búfer de color, ya que ese es el objetivo real de renderizado. Evite calcular manualmente tamaños, vistas, tijeras, etc. basándose en el tamaño indicado en QWidget o en la proporción de píxeles del dispositivo.

Nota: La matriz de proyección incluye correction matrix de QRhi para tener en cuenta las diferencias de la API 3D en las coordenadas normalizadas del dispositivo. (por ejemplo, Y hacia abajo frente a Y hacia arriba)

Se aplica una traslación de -4 sólo para asegurarse de que el triángulo con valores z de 0 será visible.

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

Renderizado

El widget registra una única pasada de renderizado, que contiene una única llamada a dibujo.

La matriz vista-proyección calculada en el paso de inicialización se combina con la matriz del modelo, que en este caso resulta ser una simple rotación. La matriz resultante se escribe en el buffer uniforme. Observe cómo resourceUpdates se pasa a beginPass(), que es un atajo para no tener que invocar resourceUpdate() manualmente.

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

En el pase de renderizado, se registra una única llamada de dibujo con 3 vértices. El canal gráfico creado en el paso de inicialización se vincula a la memoria intermedia de comandos, y la ventana gráfica se establece para cubrir todo el widget. Para hacer que el buffer uniforme sea visible para el shader (de vértices), se llama a setShaderResources() sin argumento, lo que significa usar el m_srb ya que fue asociado con el pipeline en el momento de la creación del pipeline. En renderizadores más complejos no es inusual pasar un objeto QRhiShaderResourceBindings diferente, siempre y cuando sea layout-compatible con el que se dio en el momento de la creación del pipeline. No hay búfer de índice, y hay una única vinculación de búfer de vértice (el único elemento en vbufBinding se refiere a la única entrada en la lista de vinculación del QRhiVertexInputLayout que se especificó al crear el pipeline).

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

Una vez registrado el pase de renderizado, se llama a update(). Esto solicita un nuevo fotograma, y se utiliza para asegurar que el widget se actualiza continuamente, y el triángulo aparece girando. El hilo de renderizado (el hilo principal en este caso) se ralentiza por la tasa de presentación por defecto. No hay un sistema de animación adecuado en este ejemplo, por lo que la rotación aumentará en cada fotograma, lo que significa que el triángulo girará a diferentes velocidades en pantallas con diferentes tasas de refresco.

    update();
}

Proyecto de ejemplo @ code.qt.io

Vea también QRhi, Cube RHI Widget Example, y 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.