展示如何渲染三角形使用 QRhi , Qt's 3D API and shading language abstraction layer.
Screenshot of the Simple RHI Widget example
This example is, in many ways, the counterpart of the RHI 窗口范例 在 QWidget world. The QRhiWidget subclass in this applications renders a single triangle, using a simple graphics pipeline with basic vertex and fragment shaders. Unlike the plain QWindow -based application, this example does not need to worry about lower level details, such as setting up the window and the QRhi , or dealing with swapchain and window events, as that is taken care of by the QWidget framework here. The instance of the QRhiWidget subclass is added to a QVBoxLayout . To keep the example minimal and compact, there are no further widgets or 3D content introduced.
一旦实例化
ExampleRhiWidget
,
QRhiWidget
subclass, is added to a top-level widget's child hierarchy, the corresponding window automatically becomes a Direct 3D, Vulkan, Metal, or OpenGL-rendered window. The
QPainter
-rendered widget content, i.e. everything that is not a
QRhiWidget
,
QOpenGLWidget
,或
QQuickWidget
, is then uploaded to a texture, whereas the mentioned special widgets each render to a texture. The resulting set of
textures
is composited together by the top-level widget's backingstore.
The
main()
function is quite simple. The top-level widget defaults to a size of 720p (this size is in logical units, the actual pixel size may be different, depending on the
scale factor
. The window is resizable.
QRhiWidget
makes it simple to implement subclasses that correctly deal with the resizing of the widget due to window size or layout changes.
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(); }
The QRhiWidget subclass reimplements the two virtuals: initialize () 和 render (). initialize() is called at least once before render(), but is also invoked upon a number of important changes, such as when the widget's backing texture is recreated due to a changing widget size, when render target parameters change, or when the widget changes to a new QRhi due to moving to a new top-level window.
注意:
不像
QOpenGLWidget
's legacy
initializeGL
-
resizeGL
-
paintGL
model, there are only two virtuals in
QRhiWidget
. This is because there are more special events that possible need taking care of than just resizing, e.g. when reparenting to a different top-level window. (robust
QOpenGLWidget
implementations had to deal with this by performing additional bookkeeping, e.g. by tracking the associated
QOpenGLContext
lifetime, meaning the three virtuals were not actually sufficient) A simpler pair of
initialize
-
render
,其中
initialize
is re-invoked upon important changes is better suited for this.
The
QRhi
instance is not owned by the widget. It is going to be queried in
initialize()
from the base class
. Storing it as a member allows recognizing changes when
initialize()
is invoked again. Graphics resources, such as the vertex and uniform buffers, or the graphics pipeline are however under the control of
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; };
对于
#include <rhi/qrhi.h>
statement to work, the application must link to
GuiPrivate
(或
gui-private
with qmake). See
QRhi
for more details about the compatibility promise of the
QRhi
family of APIs.
CMakeLists.txt
target_link_libraries(simplerhiwidget PRIVATE Qt6::Core Qt6::Gui Qt6::GuiPrivate Qt6::Widgets )
在
examplewidget.cpp
the widget implementation uses a helper function to load up a
QShader
object from a
.qsb
file. This application ships pre-conditioned
.qsb
files embedded in to the executable via the Qt Resource System. Due to module dependencies (and due to still supporting qmake), this example does not use the convenient CMake function
qt_add_shaders()
, but rather comes with the
.qsb
files as part of the source tree. Real world applications are encouraged to avoid this and rather use the Qt Shader Tools module's CMake integration features (
qt_add_shaders
). Regardless of the approach, in the C++ code the loading of the bundled/generated
.qsb
files is the same.
static QShader getShader(const QString &name) { QFile f(name); return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader(); }
Let's look at the initialize() implementation. First, the
QRhi
object is queried and stored for later use, and also to allow comparison in future invocations of the function. When there is a mismatch (e.g. when the widget is moved between windows), recreation of graphics resources need to be recreated is triggered by destroying and nulling out a suitable object, in this case the
m_pipeline
. The example does not actively demonstrate reparenting between windows, but it is prepared to handle it. It is also prepared to handle a changing widget size that can happen when resizing the window. That needs no special handling since
initialize()
is invoked every time that happens, and so querying
renderTarget()->pixelSize()
or
colorTexture()->pixelSize()
always gives the latest, up-to-date size in pixels. What this example is not prepared for is changing texture formats and
multisample settings
since it only ever uses the defaults (RGBA8 and no multisample antialiasing).
void ExampleRhiWidget::initialize(QRhiCommandBuffer *cb) { if (m_rhi != rhi()) { m_pipeline.reset(); m_rhi = rhi(); }
When the graphics resources need to be (re)created,
initialize()
does this using quite typical
QRhi
-based code. A single vertex buffer with the interleaved position - color vertex data is sufficient, whereas the modelview-projection matrix is exposed via a uniform buffer of 64 bytes (16 floats). The uniform buffer is the only shader visible resource, and it is only used in the vertex shader. The graphics pipeline relies on a lot of defaults (for example, depth test off, blending disabled, color write enabled, face culling disabled, the default topology of triangles, etc.) The vertex data layout is
x
,
y
,
r
,
g
,
b
, hence the stride is 5 floats, whereas the second vertex input attribute (the color) has an offset of 2 floats (skipping
x
and
y
). Each graphics pipeline has to be associated with a
QRhiRenderPassDescriptor
. This can be retrieved from the
QRhiRenderTarget
managed by the base class.
注意:
This example relies on the
QRhiWidget
's default of
autoRenderTarget
设为
true
. That is why it does not need to manage the render target, but can just query the existing one by calling
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); }
Finally, the projection matrix is calculated. This depends on the widget size and is thus done unconditionally in every invocation of the functions.
注意: Any size and viewport calculations should only ever rely on the pixel size queried from the resource serving as the color buffer since that is the actual render target. Avoid manually calculating sizes, viewports, scissors, etc. based on the QWidget -reported size or device pixel ratio.
注意: The projection matrix includes the correction matrix from QRhi in order to cater for 3D API differences in normalized device coordinates. (for example, Y down vs. Y up)
A translation of
-4
is applied just to make sure the triangle with
z
values of 0 will be 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); }
The widget records a single render pass, which contains a single draw call.
The view-projection matrix calculated in the initialize step gets combined with the model matrix, which in this case happens to be a simple rotation. The resulting matrix is then written to the uniform buffer. Note how
resourceUpdates
会被传递给
beginPass
(), which is a shortcut to not having to invoke
resourceUpdate
() manually.
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());
In the render pass, a single draw call with 3 vertices is recorded. The graphics pipeline created in the initialize step is bound on the command buffer, and the viewport is set to cover the entire widget. To make the uniform buffer visible to the (vertex) shader,
setShaderResources
() is called with no argument, which means using the
m_srb
since that was associated with the pipeline at pipeline creation time. In more complex renderers it is not unusual to pass in a different
QRhiShaderResourceBindings
object, as long as that is
layout-compatible
with the one given at pipeline creation time. There is no index buffer, and there is a single vertex buffer binding (the single element in
vbufBinding
refers to the single entry in the binding list of the
QRhiVertexInputLayout
that was specified when creating 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();
Once the render pass is recorded, update () is called. This requests a new frame, and is used to ensure the widget continuously updates, and the triangle appears rotating. The rendering thread (the main thread in this case) is throttled by the presentation rate by default. There is no proper animation system in this example, and so the rotation will increase in every frame, meaning the triangle will rotate at different speeds on displays with different refresh rates.
update(); }
另请参阅 QRhi , 立方体 RHI Widget 范例 ,和 RHI 窗口范例 .