本页内容

场景图 - 自定义材质

演示如何在Qt Quick 场景图中实现自定义材质。

该自定义材质示例演示了如何实现一个使用自定义顶点着色器和片段着色器的材质进行渲染的对象。

使用自定义 QSGMaterial 和片段着色器渲染的曼德尔布罗特分形

着色器与材质

主要功能位于片段着色器中

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

Item 和节点

我们创建一个自定义项来展示新的材质:

#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 &center)
    {
        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,并将其锚定以填充根元素。

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

为了使示例更具趣味性,我们添加了一个动画来调整缩放级别和迭代限制。中心位置保持不变。

示例项目 @ code.qt.io

© 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.