Auf dieser Seite

Szenenbaum – Benutzerdefiniertes Material

Zeigt, wie ein benutzerdefiniertes Material im Szenenbaum von „ Qt Quick “ implementiert wird.

Das Beispiel für ein benutzerdefiniertes Material zeigt, wie ein Objekt implementiert wird, das mithilfe eines Materials mit einem benutzerdefinierten Vertex- und Fragment-Shader gerendert wird.

Mandelbrot-Fraktal, gerendert mit einem benutzerdefinierten QSGMaterial und einem Fragment-Shader

Shader und Material

Die Hauptfunktionalität liegt im Fragment-Shader

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

Der Fragment- und der Vertex-Shader werden in einer Unterklasse von „ QSGMaterialShader “ zusammengefasst.

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

Eine Unterklasse von „ QSGMaterial “ kapselt den Shader zusammen mit dem Render-Status. In diesem Beispiel fügen wir Statusinformationen hinzu, die den Shader-Uniforms entsprechen. Das Material ist für die Erstellung des Shaders zuständig, indem es die Methode „ QSGMaterial::createShader()“ neu implementiert.

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

Um die Uniform-Daten zu aktualisieren, implementieren wir ` QSGMaterialShader::updateUniformData()` neu.

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

Element und Knoten

Wir erstellen ein benutzerdefiniertes Item, um unser neues Material zu präsentieren:

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

Die Deklaration von `CustomItem` fügt drei Eigenschaften hinzu, die den Uniforms entsprechen, die wir in QML verfügbar machen möchten.

    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)

Wie bei jedem benutzerdefinierten „ Qt Quick “-Item ist die Implementierung zweigeteilt: Zusätzlich zu ` CustomItem`, das im GUI-Thread läuft, erstellen wir eine Unterklasse von ` QSGNode `, die im Render-Thread läuft.

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

Der Knoten besitzt eine Instanz des Materials und verfügt über Logik zur Aktualisierung des Materialzustands. Das Element verwaltet die entsprechenden QML-Eigenschaften. Es muss die Informationen aus dem Material duplizieren, da das Element und das Material in unterschiedlichen Threads laufen.

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();
}

Die Informationen werden in einer Neuimplementierung von ` QQuickItem::updatePaintNode()` vom Item in den Szenengraphen kopiert. Die beiden Threads befinden sich bei Aufruf der Funktion an einem Synchronisationspunkt, sodass der Zugriff auf beide Klassen sicher ist.

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

Der Rest des Beispiels

Die Anwendung ist eine einfache QML-Anwendung mit einem ` QGuiApplication ` und einem ` QQuickView `, dem wir eine .qml-Datei übergeben.

In der QML-Datei erstellen wir das `customitem`, das wir so verankern, dass es den Root-Bereich ausfüllt.

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

Um das Beispiel etwas interessanter zu gestalten, fügen wir eine Animation hinzu, mit der die Zoomstufe und die Iterationsgrenze geändert werden. Die Zentrierung bleibt dabei konstant.

Beispielprojekt @ 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.