Gráfico de escenas - RHI Under QML
Muestra cómo renderizar directamente con QRhi bajo una escena Qt Quick.

Introducción
El ejemplo RHI Under QML muestra cómo una aplicación puede hacer uso de las señales QQuickWindow::beforeRendering() y QQuickWindow::beforeRenderPassRecording() para dibujar contenido personalizado basado en QRhi bajo una escena Qt Quick.
Para las aplicaciones que deseen renderizar contenido QRhi encima de la escena Qt Quick, utilice QQuickWindow::beforeRendering() para cargar datos en búferes y conectarse a la señal QQuickWindow::afterRenderPassRecording().
En este ejemplo, también veremos cómo es posible tener valores expuestos a QML que afecten al renderizado basado en QRhi. Animamos el valor umbral utilizando un NumberAnimation en el archivo QML y este valor flotante se pasa entonces en un buffer uniforme al fragment shader.
El ejemplo es equivalente en la mayoría de los aspectos a los ejemplos de OpenGL bajo QML, Direct3D 11 bajo QML, Metal bajo QML y Vulkan bajo QML. Esos ejemplos renderizan el mismo contenido utilizando directamente una API 3D. Este ejemplo, por otro lado, es totalmente multiplataforma y portátil, ya que admite de forma inherente el funcionamiento con todas las API 3D compatibles con QRhi (como OpenGL, Vulkan, Metal, Direct 3D 11 y 12).
Nota: Este ejemplo demuestra una funcionalidad avanzada de bajo nivel que realiza un renderizado 3D portable y multiplataforma, a la vez que depende de APIs con garantía de compatibilidad limitada del módulo Qt Gui. Para poder utilizar las API de QRhi, la aplicación enlaza con Qt::GuiPrivate e incluye <rhi/qrhi.h>.
Añadir renderizado personalizado como subyacente/superpuesto es una de las tres formas de integrar renderizado 2D/3D personalizado en una escena de Qt Quick. Las otras dos opciones son realizar el renderizado "en línea" con el propio renderizado de la escena Qt Quick utilizando QSGRenderNode, o generar un pase de renderizado completamente independiente dirigido a un objetivo de renderizado dedicado (una textura) y luego hacer que un elemento de la escena muestre la textura. Consulta los ejemplos Gráfico de Escena - Elemento de Textura RHI y Gráfico de Escena - Nodo de Render Personalizado QSGRenderNode.
Conceptos básicos
La señal beforeRendering() se emite al comienzo de cada fotograma, antes de que el gráfico de escena comience su renderizado, por lo que cualquier llamada a QRhi draw que se realice como respuesta a esta señal, se apilará bajo los ítems de Qt Quick. Sin embargo, hay dos señales que son relevantes aquí: los propios comandos QRhi de la aplicación deben ser grabados en el mismo buffer de comandos que es utilizado por el gráfico de escena, y lo que es más, los comandos deben pertenecer a la misma pasada de renderizado. beforeRendering() por sí sola no es suficiente para esto porque se emite al comienzo del frame, antes de comenzar a grabar una pasada de renderizado a través de QRhiCommandBuffer::beginPass(). Conectando también a beforeRenderPassRecording(), los comandos propios de la aplicación y el renderizado propio del gráfico de escena terminarán en el orden correcto:
- El bucle de renderizado del gráfico de escena llama a QRhi::beginFrame()
- QQuickWindow::beforeRendering() es emitido - la aplicación prepara los recursos para su renderizado personalizado
- La escena gráfica llama a QRhiCommandBuffer::beginPass()
- QQuickWindow::beforeRenderPassRecording() - la aplicación registra las llamadas de dibujo
- El gráfico de escena registra las llamadas de dibujo
Recorrido
El renderizado personalizado se encapsula dentro de un QQuickItem personalizado. RhiSquircle deriva de QQuickItem, y se expone a QML (nótese el QML_ELEMENT). La escena QML instancia RhiSquircle. Nótese, sin embargo, que no se trata de un elemento visual: la bandera QQuickItem::ItemHasContents no está activada. Por lo tanto, la posición y el tamaño del elemento no tienen relevancia y no reimplementa updatePaintNode().
class RhiSquircle : public QQuickItem { Q_OBJECT Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged) QML_ELEMENT public: RhiSquircle(); qreal t() const { return m_t; } void setT(qreal t); signals: void tChanged(); public slots: void sync(); void cleanup(); private slots: void handleWindowChanged(QQuickWindow *win); private: void releaseResources() override; qreal m_t = 0; SquircleRenderer *m_renderer = nullptr; };
En cambio, cuando el elemento se asocia a QQuickWindow, se conecta a la señal QQuickWindow::beforeSynchronizing(). Usar Qt::DirectConnection es importante ya que esta señal se emite en el hilo de renderizado Qt Quick, si es que hay uno. Queremos que la ranura conectada sea invocada en este mismo hilo.
RhiSquircle::RhiSquircle() { connect(this, &QQuickItem::windowChanged, this, &RhiSquircle::handleWindowChanged); } void RhiSquircle::handleWindowChanged(QQuickWindow *win) { if (win) { connect(win, &QQuickWindow::beforeSynchronizing, this, &RhiSquircle::sync, Qt::DirectConnection); connect(win, &QQuickWindow::sceneGraphInvalidated, this, &RhiSquircle::cleanup, Qt::DirectConnection); // Ensure we start with cleared to black. The squircle's blend mode relies on this. win->setColor(Qt::black); } }
En la fase de sincronización del gráfico de escena, se crea la infraestructura de renderizado, si aún no se ha hecho, y se sincronizan los datos relevantes para el renderizado, es decir, se copian del elemento RhiSquircle, que vive en el hilo principal, al objeto SquircleRenderer que vive en el hilo de renderizado. (El acceso a los datos es seguro porque el hilo principal está bloqueado mientras el hilo de renderizado ejecuta su fase de sincronización. Ver Qt Quick Scene Graph para más información sobre el modelo de renderizado y threading del gráfico de escena.
Además del valor de t, también se copia el puntero QQuickWindow asociado. Aunque SquircleRenderer podría consultar window() en el elemento RhiSquircle incluso cuando opera en el hilo de renderizado, eso no es, en teoría, del todo seguro. De ahí que se haga una copia.
Al configurar SquircleRenderer, se realizan conexiones con beforeRendering() y beforeRenderPassRecording(), que son la clave para poder actuar e inyectar los comandos de renderizado 3D personalizados de la aplicación en el momento adecuado.
void RhiSquircle::sync() { // This function is invoked on the render thread, if there is one. if (!m_renderer) { m_renderer = new SquircleRenderer; // Initializing resources is done before starting to record the // renderpass, regardless of wanting an underlay or overlay. connect(window(), &QQuickWindow::beforeRendering, m_renderer, &SquircleRenderer::frameStart, Qt::DirectConnection); // Here we want an underlay and therefore connect to // beforeRenderPassRecording. Changing to afterRenderPassRecording // would render the squircle on top (overlay). connect(window(), &QQuickWindow::beforeRenderPassRecording, m_renderer, &SquircleRenderer::mainPassRecordingStart, Qt::DirectConnection); } m_renderer->setT(m_t); m_renderer->setWindow(window()); }
Cuando se emite beforeRendering(), se crean los recursos QRhi necesarios para nuestro renderizado personalizado, como QRhiBuffer, QRhiGraphicsPipeline, y objetos relacionados, si aún no se han hecho.
Los datos de los búferes se actualizan (más concretamente, las operaciones de actualización de datos se ponen en cola) utilizando QRhiResourceUpdateBatch y QRhiCommandBuffer::resourceUpdate(). El búfer de vértices no cambia su contenido una vez que se ha cargado en él el conjunto inicial de vértices. Sin embargo, el búfer uniforme es un búfer dynamic, como es típico en este tipo de búferes. Su contenido, al menos algunas regiones, se actualiza en cada fotograma. De ahí la llamada incondicional a updateDynamicBuffer() para el offset 0 y un tamaño de byte de 4 (que es sizeof(float) ya que el tipo de C++ float resulta coincidir con el de 32 bits de GLSL float). Lo que se almacena en esa posición es el valor de t, y eso se actualiza en cada fotograma, es decir, en cada invocación de frameStart().
Hay un valor float adicional en el buffer, comenzando en el offset 4. Esto se utiliza para atender a las diferencias del sistema de coordenadas de las APIs 3D: cuando isYUpInNDC() devuelve false, que es el caso de Vulkan en particular, el valor se establece en -1,0 lo que lleva a voltear el valor Y en el vector de 2 componentes que se pasa (con interpolación) al fragment shader basado en el cual se calcula el color. De esta manera la salida en la pantalla es idéntica (es decir, la esquina superior izquierda es de color verde, la parte inferior izquierda es de color rojo), independientemente de la API 3D que se esté utilizando. Este valor sólo se actualiza una vez en el búfer uniforme, de forma similar al búfer de vértices. Esto pone de relieve un problema con el que a menudo tiene que lidiar el código de renderizado de bajo nivel que pretende ser portátil: las diferencias en el sistema de coordenadas en las coordenadas normalizadas del dispositivo (NDC) y en las imágenes y framebuffers. Por ejemplo, el NDC utiliza un sistema de origen en la parte inferior izquierda en todas partes excepto en Vulkan. Mientras que los framebuffers utilizan un sistema de origen-arriba-izquierda en todas partes excepto OpenGL. Los renderizadores típicos que trabajan con una proyección en perspectiva a menudo pueden ser ajenos a este problema confiando convenientemente en QRhi::clipSpaceCorrMatrix(), que es una matriz que puede ser multiplicada en la matriz de proyección, y aplica tanto un giro Y cuando es necesario, y también atiende al hecho de que la profundidad del espacio de recorte se ejecuta -1..1 con OpenGL pero 0..1 en todas partes. Sin embargo, en algunos casos, como en este ejemplo, esto no es aplicable. En su lugar, la aplicación y la lógica del sombreador necesitan realizar el ajuste necesario de las posiciones de vértices y UV según corresponda basándose en la consulta a QRhi::isYUpInNDC() y QRhi::isYUpInFramebuffer().
Para obtener acceso a los objetos QRhi y QRhiSwapChain que utiliza Qt Quick, simplemente se pueden consultar desde QQuickWindow. Tenga en cuenta que esto supone que QQuickWindow es una ventana normal en pantalla. Si se utilizara QQuickRenderControl en su lugar, por ejemplo, para realizar un renderizado fuera de pantalla en una textura, la consulta de la cadena de intercambio sería errónea, ya que entonces no hay cadena de intercambio.
Debido a la señal que se emite después de que Qt Quick llame a QRhi::beginFrame(), ya es posible consultar el buffer de comandos y el objetivo de renderizado desde la swapchain. Esto es lo que permite emitir convenientemente un QRhiCommandBuffer::resourceUpdate() sobre el objeto devuelto por QRhiSwapChain::currentFrameCommandBuffer(). Cuando se crea un conducto de gráficos, se puede recuperar un QRhiRenderPassDescriptor del QRhiRenderTarget devuelto desde QRhiSwapChain::currentFrameRenderTarget(). (ten en cuenta que esto significa que el conducto gráfico construido aquí es adecuado sólo para renderizar a la swapchain, o en el mejor de los casos a otro objetivo de renderizado que sea compatible con ella; es probable que si quisiéramos renderizar a una textura, entonces se necesitaría un QRhiRenderPassDescriptor diferente, y por lo tanto un conducto gráfico diferente, ya que los formatos de textura y swapchain pueden diferir).
void SquircleRenderer::frameStart() { // Esta función se invoca en el hilo de renderizado, si existe. QRhi *rhi = m_window->rhi(); if (!rhi) { qWarning("QQuickWindow is not using QRhi for rendering"); return; } QRhiSwapChain *swapChain = m_window->swapChain(); if (!swapChain) { qWarning("No QRhiSwapChain?"); return; } QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch(); if (!m_pipeline) { m_vertexShader = getShader(QLatin1String(":/scenegraph/rhiunderqml/squircle_rhi.vert.qsb")); if (!m_vertexShader.isValid()) qWarning("Failed to load vertex shader; rendering will be incorrect"); m_fragmentShader = getShader(QLatin1String(":/scenegraph/rhiunderqml/squircle_rhi.frag.qsb")); if (!m_fragmentShader.isValid()) qWarning("Failed to load fragment shader; rendering will be incorrect"); m_vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Inmutable, QRhiBuffer::VertexBuffer, sizeof(vertices))); m_vertexBuffer->create(); resourceUpdates->uploadStaticBuffer(m_vertexBuffer.get(), vertices); const quint32 UBUF_SIZE = 4 + 4; // 2 floatsm_uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dinámico, QRhiBuffer::UniformBuffer, UBUF_SIZE)); m_uniformBuffer->create(); float yDir = rhi->isYUpInNDC() ? 1.0f:-1.0f; resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 4, 4, &yDir); m_srb.reset(rhi->newShaderResourceBindings()); const auto visibleToAll = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage; m_srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, visibleToAll, m_uniformBuffer.get()) }); m_srb->create(); QRhiVertexInputLayout inputLayout; inputLayout.setBindings({ { 2 * sizeof(float) } }); inputLayout.setAttributes({ 0, 0, QRhiVertexInputAttribute::Float2, 0 } }); m_pipeline.reset(rhi->newGraphicsPipeline()); m_pipeline->setTopology(QRhiGraphicsPipeline::TriánguloTira); QRhiGraphicsPipeline::TargetBlend blend; blend.enable = true; blend.srcColor = QRhiGraphicsPipeline::SrcAlpha; blend.srcAlpha = QRhiGraphicsPipeline::SrcAlpha; blend.dstColor = QRhiGraphicsPipeline::Uno; blend.dstAlpha = QRhiGraphicsPipeline::Uno; m_pipeline->setTargetBlends({ blend }); m_pipeline->setShaderStages({ QRhiShaderStage::Vértice, m_vertexShader },{ QRhiShaderStage::Fragment, m_fragmentShader } }); m_pipeline->setVertexInputLayout(inputLayout); m_pipeline->setShaderResourceBindings(m_srb.get()); m_pipeline->setRenderPassDescriptor(swapChain->currentFrameRenderTarget()->renderPassDescriptor()); m_pipeline->create(); } float t = m_t; resourceUpdates->updateDynamicBuffer(m_uniformBuffer.get(), 0, 4, &t); swapChain->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates); }
Finalmente, en QQuickWindow::beforeRenderPassRecording(), se graba una llamada a dibujar una tira triangular con 4 vértices. Este ejemplo simplemente dibuja un cuadrado en la práctica, y calcula los colores de los píxeles usando la lógica en los fragment shaders, pero las aplicaciones son libres de hacer dibujos más complicados: crear múltiples pipelines gráficos y grabar múltiples llamadas a dibujar está perfectamente bien también. Lo importante a tener en cuenta es que todo lo que se graba en QRhiCommandBuffer recuperado de la ventana swapchain, se antepone efectivamente al propio renderizado del gráfico de escena Qt Quick dentro del pase de render principal.
Nota: Esto significa que si se utiliza el búfer de profundidad con pruebas de profundidad y escritura de valores de profundidad, entonces el contenido de Qt Quick puede verse afectado por los valores escritos en el búfer de profundidad. Ver Qt Quick Scene Graph Default Renderer para más detalles sobre el renderizador del gráfico de escena, en particular las secciones sobre el manejo de primitivas opacas y alpha blended.
Para obtener el tamaño de la ventana en píxeles, se utiliza QRhiRenderTarget::pixelSize(). Esto es conveniente porque de esta manera el ejemplo no necesita calcular el tamaño de la ventana gráfica por otros medios y no tiene que preocuparse de aplicar el high DPI scale factor, si lo hay.
void SquircleRenderer::mainPassRecordingStart() { // This function is invoked on the render thread, if there is one. QRhi *rhi = m_window->rhi(); QRhiSwapChain *swapChain = m_window->swapChain(); if (!rhi || !swapChain) return; const QSize outputPixelSize = swapChain->currentFrameRenderTarget()->pixelSize(); QRhiCommandBuffer *cb = m_window->swapChain()->currentFrameCommandBuffer(); cb->setViewport({ 0.0f, 0.0f, float(outputPixelSize.width()), float(outputPixelSize.height()) }); cb->setGraphicsPipeline(m_pipeline.get()); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuffer.get(), 0); cb->setVertexInput(0, 1, &vbufBinding); cb->draw(4); }
Los sombreadores de vértices y fragmentos pasan por el pipeline estándar de acondicionamiento de sombreadores de QRhi. Inicialmente escritos como GLSL compatible con Vulkan, son compilados a SPIR-V y luego transpilados a otros lenguajes de sombreado por las herramientas de Qt. Cuando se utiliza CMake, el ejemplo se basa en el comando qt_add_shaders que hace que sea simple y conveniente para agrupar los sombreadores con la aplicación y realizar el procesamiento necesario en tiempo de compilación. Ver Qt Shader Tools Build System Integration para más detalles.
Especificar BASE ayuda a eliminar el prefijo ../shared, mientras que PREFIX añade el prefijo /scenegraph/rhiunderqml. Así, la ruta final es :/scenegraph/rhiunderqml/squircle_rhi.vert.qsb.
qt_add_shaders(rhiunderqml "rhiunderqml_shaders"
PRECOMPILE
OPTIMIZED
PREFIX
/scenegraph/rhiunderqml
BASE
../shared
FILES
../shared/squircle_rhi.vert
../shared/squircle_rhi.frag
)Para soportar qmake, el ejemplo aún incluye los archivos .qsb que normalmente se generarían en tiempo de compilación, y los lista en el archivo qrc. Sin embargo, este método no es recomendable para nuevas aplicaciones que utilicen CMake como sistema de compilación.
Ver también SceneGraph - RHI Texture Item y Scene Graph - Custom QSGRenderNode.
© 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.