이 페이지에서

씬 그래프 - 사용자 정의 머티리얼

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

아이템과 노드

새로운 머티리얼을 보여주기 위해 커스텀 아이템을 생성합니다:

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