场景图 - 自定义材质
展示如何在Qt Quick 场景图中实现自定义材质。
自定义材质示例展示了如何使用带有自定义顶点和片段着色器的材质来渲染一个项目。
着色器和材质
主要功能在片段着色器中
// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #version 440 layout(location = 0) in vec2 vTexCoord; layout(location = 0) out vec4 fragColor; // uniform block: 84 bytes layout(std140, binding = 0) uniform buf { mat4 qt_Matrix; // offset 0 float qt_Opacity; // offset 64 float zoom; // offset 68 vec2 center; // offset 72 int limit; // offset 80 } ubuf; void main() { vec4 color1 = vec4(1.0, 0.85, 0.55, 1); vec4 color2 = vec4(0.226, 0.0, 0.615, 1); float aspect_ratio = -ubuf.qt_Matrix[0][0]/ubuf.qt_Matrix[1][1]; vec2 z, c; c.x = (vTexCoord.x - 0.5) / ubuf.zoom + ubuf.center.x; c.y = aspect_ratio * (vTexCoord.y - 0.5) / ubuf.zoom + ubuf.center.y; int iLast; z = c; for (int i = 0; i < 1000000; i++) { if (i >= ubuf.limit) { iLast = i; break; } float x = (z.x * z.x - z.y * z.y) + c.x; float y = (z.y * z.x + z.x * z.y) + c.y; if ((x * x + y * y) > 4.0) { iLast = i; break; } z.x = x; z.y = y; } if (iLast == ubuf.limit) { fragColor = vec4(0.0, 0.0, 0.0, 1.0); } else { float f = (iLast * 1.0) / ubuf.limit; fragColor = mix(color1, color2, sqrt(f)); } }
片段着色器和顶点着色器合并为一个QSGMaterialShader 子类。
class CustomShader : public QSGMaterialShader { public: CustomShader() { setShaderFileName(VertexStage, QLatin1String(":/scenegraph/custommaterial/shaders/mandelbrot.vert.qsb")); setShaderFileName(FragmentStage, QLatin1String(":/scenegraph/custommaterial/shaders/mandelbrot.frag.qsb")); } bool updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; };
QSGMaterial 子类封装了着色器和渲染状态。在本示例中,我们添加了与着色器制服相对应的状态信息。材质负责通过重新实现QSGMaterial::createShader() 来创建着色器。
class CustomMaterial : public QSGMaterial { public: CustomMaterial(); QSGMaterialType *type() const override; int compare(const QSGMaterial *other) const override; QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override { return new CustomShader; } struct { float center[2]; float zoom; int limit; bool dirty; } uniforms; };
要更新制服数据,我们要重新实现QSGMaterialShader::updateUniformData().
bool CustomShader::updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) { bool changed = false; QByteArray *buf = state.uniformData(); Q_ASSERT(buf->size() >= 84); if (state.isMatrixDirty()) { const QMatrix4x4 m = state.combinedMatrix(); memcpy(buf->data(), m.constData(), 64); changed = true; } if (state.isOpacityDirty()) { const float opacity = state.opacity(); memcpy(buf->data() + 64, &opacity, 4); changed = true; } auto *customMaterial = static_cast<CustomMaterial *>(newMaterial); if (oldMaterial != newMaterial || customMaterial->uniforms.dirty) { memcpy(buf->data() + 68, &customMaterial->uniforms.zoom, 4); memcpy(buf->data() + 72, &customMaterial->uniforms.center, 8); memcpy(buf->data() + 80, &customMaterial->uniforms.limit, 4); customMaterial->uniforms.dirty = false; changed = true; } return changed; }
项目和节点
我们创建一个自定义项目来展示我们的新材质:
#include <QQuickItem> class CustomItem : public QQuickItem { Q_OBJECT Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged) Q_PROPERTY(int iterationLimit READ iterationLimit WRITE setIterationLimit NOTIFY iterationLimitChanged) Q_PROPERTY(QPointF center READ center WRITE setCenter NOTIFY centerChanged) QML_ELEMENT public: explicit CustomItem(QQuickItem *parent = nullptr); qreal zoom() const { return m_zoom; } int iterationLimit() const { return m_limit; } QPointF center() const { return m_center; } public slots: void setZoom(qreal zoom); void setIterationLimit(int iterationLimit); void setCenter(QPointF center); signals: void zoomChanged(qreal zoom); void iterationLimitChanged(int iterationLimit); void centerChanged(QPointF center); protected: QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override; void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; private: bool m_geometryChanged = true; qreal m_zoom; bool m_zoomChanged = true; int m_limit; bool m_limitChanged = true; QPointF m_center; bool m_centerChanged = true; };
CustomItem 声明添加了三个属性,分别对应于我们要向 QML 公开的制服。
Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged) Q_PROPERTY(int iterationLimit READ iterationLimit WRITE setIterationLimit NOTIFY iterationLimitChanged) Q_PROPERTY(QPointF center READ center WRITE setCenter NOTIFY centerChanged)
与每个自定义Qt Quick 项目一样,实现也是一分为二的:除了在 GUI 线程中运行的CustomItem
,我们还创建了一个在渲染线程中运行的QSGNode 子类。
class CustomNode : public QSGGeometryNode { public: CustomNode() { auto *m = new CustomMaterial; setMaterial(m); setFlag(OwnsMaterial, true); QSGGeometry *g = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); QSGGeometry::updateTexturedRectGeometry(g, QRect(), QRect()); setGeometry(g); setFlag(OwnsGeometry, true); } void setRect(const QRectF &bounds) { QSGGeometry::updateTexturedRectGeometry(geometry(), bounds, QRectF(0, 0, 1, 1)); markDirty(QSGNode::DirtyGeometry); } void setZoom(qreal zoom) { auto *m = static_cast<CustomMaterial *>(material()); m->uniforms.zoom = zoom; m->uniforms.dirty = true; markDirty(DirtyMaterial); } void setLimit(int limit) { auto *m = static_cast<CustomMaterial *>(material()); m->uniforms.limit = limit; m->uniforms.dirty = true; markDirty(DirtyMaterial); } void setCenter(const QPointF ¢er) { auto *m = static_cast<CustomMaterial *>(material()); m->uniforms.center[0] = center.x(); m->uniforms.center[1] = center.y(); m->uniforms.dirty = true; markDirty(DirtyMaterial); } };
该节点拥有一个材质实例,并具有更新材质状态的逻辑。项目维护相应的 QML 属性。由于项目和材质生活在不同的线程中,它需要复制材质的信息。
void CustomItem::setZoom(qreal zoom) { if (qFuzzyCompare(m_zoom, zoom)) return; m_zoom = zoom; m_zoomChanged = true; emit zoomChanged(m_zoom); update(); } void CustomItem::setIterationLimit(int limit) { if (m_limit == limit) return; m_limit = limit; m_limitChanged = true; emit iterationLimitChanged(m_limit); update(); } void CustomItem::setCenter(QPointF center) { if (m_center == center) return; m_center = center; m_centerChanged = true; emit centerChanged(m_center); update(); }
在重新实现QQuickItem::updatePaintNode() 时,信息会从项目复制到场景图。调用该函数时,两个线程处于同步点,因此访问两个类都是安全的。
QSGNode *CustomItem::updatePaintNode(QSGNode *old, UpdatePaintNodeData *) { auto *node = static_cast<CustomNode *>(old); if (!node) node = new CustomNode; if (m_geometryChanged) node->setRect(boundingRect()); m_geometryChanged = false; if (m_zoomChanged) node->setZoom(m_zoom); m_zoomChanged = false; if (m_limitChanged) node->setLimit(m_limit); m_limitChanged = false; if (m_centerChanged) node->setCenter(m_center); m_centerChanged = false; return node; }
示例的其余部分
该应用程序是一个简单的 QML 应用程序,它有一个QGuiApplication 和一个QQuickView ,我们将其传递给一个 .qml 文件。
在 QML 文件中,我们创建了用来填充根目录的自定义项目。
CustomItem { property real t: 1 anchors.fill: parent center: Qt.point(-0.748, 0.1); iterationLimit: 3 * (zoom + 30) zoom: t * t / 10 NumberAnimation on t { from: 1 to: 60 duration: 30*1000; running: true loops: Animation.Infinite } }
为了让示例更有趣,我们添加了一个动画来改变缩放级别和迭代限制。中心保持不变。
© 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.