Cubo RHI Widget Ejemplo
Muestra cómo renderizar un cubo texturizado e integrarlo con QPainter y widgets, utilizando la API 3D de QRhi y la capa de abstracción del lenguaje de sombreado.

Captura de pantalla del ejemplo del widget Cube RHI
Este ejemplo se basa en el ejemplo simple de widget RHI. Mientras que el ejemplo simple es intencionadamente mínimo y lo más compacto posible, renderizando sólo un único triángulo sin widgets adicionales en la ventana, esta aplicación lo demuestra:
- Tener varios widgets en la ventana, algunos de ellos controlando datos que son consumidos por la subclase QRhiWidget.
- En lugar de solicitar actualizaciones continuamente, el QRhiWidget aquí sólo actualiza el contenido en su textura de respaldo cuando algunos datos relacionados cambian.
- El cubo se texturiza utilizando un QRhiTexture que obtiene su contenido de un QImage que contiene renderizado basado en software realizado con QPainter.
- El contenido del QRhiWidget can be read back y se guarda en un archivo de imagen (por ejemplo, un archivo PNG).
- 4x antialiasing multimuestra can be toggled en tiempo de ejecución. La subclase QRhiWidget está preparada para manejar correctamente el recuento cambiante de muestras.
- Forzar un explicitly specified backing texture size puede ser activado dinámicamente y controlado con un deslizador entre 16x16 hasta 512x512 píxeles.
- La subclase QRhiWidget maneja correctamente un QRhi cambiante. Esto se puede ver en acción cuando se hace que el widget sea de nivel superior (sin padre; se convierte en una ventana separada) y luego se reparenting de nuevo en la jerarquía hijo de la ventana principal.
- Y lo que es más importante, algunos widgets, incluso con semitransparencia, pueden colocarse encima de QRhiWidget, lo que demuestra que es posible apilarlos y mezclarlos correctamente. Este es un caso en el que QRhiWidget es superior a la incrustación de una ventana nativa, es decir, una basada en QRhi QWindow utilizando QWidget::createWindowContainer(), porque permite apilar y recortar de la misma manera que cualquier otra ventana normal renderizada por software QWidget, mientras que la incrustación de ventanas nativas puede tener, dependiendo de la plataforma, varias limitaciones, por ejemplo, a menudo puede ser difícil o ineficiente colocar controles adicionales encima.
En la reimplementación de initialize(), lo primero que hay que hacer es comprobar si el QRhi con el que trabajamos la última vez sigue actualizado, y si el recuento de muestras (para el antialiasing multimuestra) ha cambiado. Lo primero es importante porque todos los recursos gráficos deben ser liberados cuando el QRhi cambia, mientras que con un recuento de muestras que cambia dinámicamente surge un problema similar específicamente para los objetos QRhiGraphicsPipeline como los que hornean el recuento de muestras en. Para simplificar, la aplicación maneja todos estos cambios de la misma manera, reseteando su estructura scene a una construida por defecto, que convenientemente libera todos los recursos gráficos. Todos los recursos son entonces recreados.
Cuando el tamaño de la textura de apoyo (por lo tanto el tamaño del objetivo de renderizado) cambia, no se necesita ninguna acción especial, pero se emite una señal por conveniencia, sólo para que main() pueda reposicionar la etiqueta de superposición. El nombre de la API 3D también se expone a través de una señal consultando QRhi::backendName() cada vez que cambia QRhi.
La implementación tiene que ser consciente de que el antialiasing multimuestra implica que colorTexture() es nullptr, mientras que msaaColorBuffer() es válido. Esto es lo contrario de cuando el MSAA no está en uso. La razón de diferenciar y usar distintos tipos (QRhiTexture, QRhiRenderBuffer) es permitir usar MSAA con APIs de gráficos 3D que no tienen soporte para texturas multimuestra, pero sí para renderbuffers multimuestra. Un ejemplo de esto es OpenGL ES 3.0.
Para comprobar el tamaño de píxel y el número de muestras actualizados, una solución cómoda y compacta es realizar la consulta a través de QRhiRenderTarget, ya que de esta forma no es necesario comprobar cuáles de las funciones colorTexture() y msaaColorBuffer() son válidas.
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 = {}; }
El resto se explica por sí mismo. Los buffers y pipelines son (re)creados, si es necesario. Se actualiza el contenido de la textura que se utiliza para texturizar la malla del cubo. La escena se renderiza utilizando una proyección en perspectiva. La vista es por ahora una simple traslación.
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(); }
La función que realiza la cola de escritura del buffer uniforme también tiene en cuenta la rotación proporcionada por el usuario, generando así la matriz final modelview-projection.
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()); }
Actualizar el QRhiTexture que es muestreado en el fragment shader cuando se renderiza el cubo, es bastante simple, a pesar de que mucho está sucediendo allí: primero se genera un dibujo basado en QPainter dentro de un QImage. Esto utiliza el texto proporcionado por el usuario. A continuación, los datos de píxeles del lado de la CPU se cargan en una textura (más concretamente, la operación de carga se graba en un QRhiResourceUpdateBatch, que se envía posteriormente en 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); }
La inicialización de los recursos gráficos es sencilla. Sólo hay un búfer de vértices, ningún búfer de índice, y un búfer uniforme con sólo una matriz 4x4 en él (16 floats).
La textura que contiene el dibujo generado por QPainter tiene un tamaño de 512x512. Tenga en cuenta que todos los tamaños (tamaños de textura, viewports, tijeras, regiones de carga de textura, etc.) están siempre en píxeles cuando se trabaja con QRhi. Para muestrear esta textura en el shader, se necesita un sampler object (independientemente del hecho de que las aplicaciones basadas en QRhi normalmente usarán muestreadores de imagen combinados en el código del shader GLSL, que luego pueden ser transpilados a objetos separados de textura y muestreador con algunos lenguajes de sombreado, o pueden permanecer como un objeto combinado de textura-muestreador con otros, lo que significa que puede no haber realmente un objeto muestreador nativo bajo el capó en tiempo de ejecución, dependiendo de la API 3D, pero todo esto es transparente para la aplicación).
El sombreador de vértices lee del búfer uniforme en el punto de enlace 0, por lo que scene.ubuf se expone en esa ubicación de enlace. El sombreador de fragmentos muestrea una textura proporcionada en el punto de enlace 1, por lo que se especifica un par combinado de textura-muestreador para esa ubicación de enlace.
QRhiGraphicsPipeline permite la prueba/escritura de profundidad y elimina las caras posteriores. También se basa en una serie de valores por defecto, por ejemplo, la función de comparación de profundidad por defecto a Less, que está bien para nosotros, y el modo de la cara frontal es en sentido antihorario, que también es bueno tal cual, por lo que no es necesario establecer de nuevo.
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();
En la reimplementación de render(), primero se comprueban los datos proporcionados por el usuario. Si el QSlider que controla la rotación ha proporcionado un nuevo valor, o el QTextEdit con el texto del cubo ha cambiado su texto, los recursos gráficos cuyo contenido depende de esos datos se actualizan.
A continuación, se registra una única pasada de renderizado con una única llamada a dibujo. Los datos de la malla del cubo se proporcionan en un formato no intercalado, de ahí la necesidad de dos enlaces de entrada de vértices, uno son las posiciones (x, y, z) el otro son las UVs (u, v), con un desplazamiento de inicio que corresponde a 36 pares flotantes 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(); }
¿Cómo se envían los datos proporcionados por el usuario? Tomemos como ejemplo la rotación. main() se conecta a la señal valueChanged de QSlider. Cuando se emite, la lamda conectada llama a setCubeRotation() en el ExampleRhiWidget. Aquí, si el valor es diferente del anterior, se almacena, y se establece una bandera dirty. Entonces, lo más importante, update() es llamado en el ExampleRhiWidget. Esto es lo que desencadena el renderizado de un nuevo fotograma en la textura de fondo de QRhiWidget. Sin esto el contenido del ExampleRhiWidget no se actualizaría al arrastrar el deslizador.
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(); }
Ver también QRhi, Simple 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.