씬 그래프 - Metal 텍스처 가져오기

Metal을 사용하여 직접 생성한 텍스처를 사용하는 방법을 보여줍니다.

Squircle을 Metal 텍스처로 렌더링하여 <span translate=Qt Quick 항목에 표시" src="images/metaltextureimport-example.jpg" title="Squircle을 Metal 텍스처로 렌더링하여 Qt Quick 항목에 표시"/>

Metal 텍스처 임포트 예제는 애플리케이션이 ‘ Qt Quick ’ 장면에서 MTLTexture를 임포트하고 사용하는 방법을 보여줍니다. 이는 네이티브 Metal 렌더링을 통합할 때 언더레이(underlay ) 또는 오버레이(overlay) 방식에 대한 대안을 제공합니다. 대부분의 경우, 텍스처를 경유하여 3D 콘텐츠를 먼저 “평면화”하는 것이 Qt Quick 에서 제공하는 2D UI 요소와 사용자 정의 3D 콘텐츠를 통합하고 혼합하는 데 가장 좋은 방법입니다.

import MetalTextureImport
CustomTextureItem {
    id: renderer
    anchors.fill: parent
    anchors.margins: 10

    SequentialAnimation on t {
        NumberAnimation { to: 1; duration: 2500; easing.type: Easing.InQuad }
        NumberAnimation { to: 0; duration: 2500; easing.type: Easing.OutQuad }
        loops: Animation.Infinite
        running: true
    }

이 애플리케이션은 CustomTextureItem이라는 이름으로 사용자 정의 QQuickItem 하위 클래스를 노출합니다. 이 클래스는 QML에서 인스턴스화됩니다. t 속성의 값도 애니메이션 처리됩니다.

class CustomTextureItem : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged)
    QML_ELEMENT

public:
    CustomTextureItem();

    qreal t() const { return m_t; }
    void setT(qreal t);

signals:
    void tChanged();

protected:
    QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override;
    void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;

private slots:
    void invalidateSceneGraph();

private:
    void releaseResources() override;

    CustomTextureNode *m_node = nullptr;
    qreal m_t = 0;
};

사용자 정의 아이템을 구현하려면 QQuickItem::updatePaintNode() 메서드를 재정의해야 할 뿐만 아니라, 기하학적 변경 및 정리와 관련된 함수와 슬롯도 재정의해야 합니다.

class CustomTextureNode : public QSGTextureProvider, public QSGSimpleTextureNode
{
    Q_OBJECT

public:
    CustomTextureNode(QQuickItem *item);
    ~CustomTextureNode();

    QSGTexture *texture() const override;

    void sync();

또한 씬그래프 노드도 필요합니다. QSGNode 를 직접 상속받는 대신, 편의상 일부 기능이 미리 구현된 QSGSimpleTextureNode 를 사용할 수 있습니다.

QSGNode *CustomTextureItem::updatePaintNode(QSGNode *node, UpdatePaintNodeData *)
{
    CustomTextureNode *n = static_cast<CustomTextureNode *>(node);

    if (!n && (width() <= 0 || height() <= 0))
        return nullptr;

    if (!n) {
        m_node = new CustomTextureNode(this);
        n = m_node;
    }

    m_node->sync();

    n->setTextureCoordinatesTransform(QSGSimpleTextureNode::NoTransform);
    n->setFiltering(QSGTexture::Linear);
    n->setRect(0, 0, width(), height());

    window()->update(); // ensure getting to beforeRendering() at some point

    return n;
}

아이템의 updatePaintNode() 함수는 (존재할 경우) 렌더링 스레드에서 호출되며, 이 때 메인(GUI) 스레드는 차단됩니다. 여기서는 아직 노드가 생성되지 않은 경우 새 노드를 생성하고, 이를 업데이트합니다. 이곳에서는 메인 스레드에 존재하는 Qt 객체에 접근하는 것이 안전하므로, sync()는 QQuickItem 또는 QQuickWindow 에서 필요한 값을 계산하고 복사합니다.

CustomTextureNode::CustomTextureNode(QQuickItem *item)
    : m_item(item)
{
    m_window = m_item->window();
    connect(m_window, &QQuickWindow::beforeRendering, this, &CustomTextureNode::render);
    connect(m_window, &QQuickWindow::screenChanged, this, [this]() {
        if (m_window->effectiveDevicePixelRatio() != m_dpr)
            m_item->update();
    });

이 노드는 단순히 일반적인 QQuickItem - QSGNode 업데이트 순서에만 의존하는 것이 아니라, QQuickWindow::beforeRendering()에도 연결됩니다. 이곳에서 Qt Quick 씬그래프의 명령 버퍼에 해당 텍스처를 대상으로 한 전체 렌더링 패스를 인코딩하여 Metal 텍스처의 내용이 업데이트됩니다. beforeRendering()은 Qt Quick 가 자체 렌더링 명령을 인코딩하기 전에 신호가 발산되므로 이를 수행하기에 적합한 위치입니다. 이 예제에서 QQuickWindow::beforeRenderPassRecording()을 대신 선택한다면 오류가 발생합니다.

void CustomTextureNode::sync()
{
    m_dpr = m_window->effectiveDevicePixelRatio();
    const QSize newSize = m_window->size() * m_dpr;
    bool needsNew = false;

    if (!texture())
        needsNew = true;

    if (newSize != m_size) {
        needsNew = true;
        m_size = newSize;
    }

    if (needsNew) {
        delete texture();
        [m_texture release];

        QSGRendererInterface *rif = m_window->rendererInterface();
        m_device = (id<MTLDevice>) rif->getResource(m_window, QSGRendererInterface::DeviceResource);
        Q_ASSERT(m_device);

        MTLTextureDescriptor *desc = [[MTLTextureDescriptor alloc] init];
        desc.textureType = MTLTextureType2D;
        desc.pixelFormat = MTLPixelFormatRGBA8Unorm;
        desc.width = m_size.width();
        desc.height = m_size.height();
        desc.mipmapLevelCount = 1;
        desc.resourceOptions = MTLResourceStorageModePrivate;
        desc.storageMode = MTLStorageModePrivate;
        desc.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget;
        m_texture = [m_device newTextureWithDescriptor: desc];
        [desc release];

        QSGTexture *wrapper = QNativeInterface::QSGMetalTexture::fromNative(m_texture, m_window, m_size);

        qDebug() << "Got QSGTexture wrapper" << wrapper << "for an MTLTexture of size" << m_size;

        setTexture(wrapper);
    }
    m_t = float(static_cast<CustomTextureItem *>(m_item)->t());

필요한 값을 복사한 후, sync()는 그래픽 리소스 초기화 작업도 수행합니다. 시네그래프에서 MTLDevice를 조회합니다. MTLTexture를 사용할 수 있게 되면, QNativeInterface::QSGOpenGLTexture::fromNative()를 통해 이를 래핑(소유하지 않음)하는 QSGTexture 가 생성됩니다. 마지막으로, base 클래스의 setTexture() 함수를 호출하여 QSGTexture 를 기본 머티리얼과 연결합니다.

void CustomTextureNode::render()
{
    if (!m_initialized)
        return;

    // Render to m_texture.
    MTLRenderPassDescriptor *renderpassdesc = [MTLRenderPassDescriptor renderPassDescriptor];
    MTLClearColor c = MTLClearColorMake(0, 0, 0, 1);
    renderpassdesc.colorAttachments[0].loadAction = MTLLoadActionClear;
    renderpassdesc.colorAttachments[0].storeAction = MTLStoreActionStore;
    renderpassdesc.colorAttachments[0].clearColor = c;
    renderpassdesc.colorAttachments[0].texture = m_texture;

    QSGRendererInterface *rif = m_window->rendererInterface();
    id<MTLCommandBuffer> cb = (id<MTLCommandBuffer>) rif->getResource(m_window, QSGRendererInterface::CommandListResource);
    Q_ASSERT(cb);
    id<MTLRenderCommandEncoder> encoder = [cb renderCommandEncoderWithDescriptor: renderpassdesc];

    const QQuickWindow::GraphicsStateInfo &stateInfo(m_window->graphicsStateInfo());
    void *p = [m_ubuf[stateInfo.currentFrameSlot] contents];
    memcpy(p, &m_t, 4);

    MTLViewport vp;
    vp.originX = 0;
    vp.originY = 0;
    vp.width = m_size.width();
    vp.height = m_size.height();
    vp.znear = 0;
    vp.zfar = 1;
    [encoder setViewport: vp];

    [encoder setFragmentBuffer: m_ubuf[stateInfo.currentFrameSlot] offset: 0 atIndex: 0];
    [encoder setVertexBuffer: m_vbuf offset: 0 atIndex: 1];
    [encoder setRenderPipelineState: m_pipeline];
    [encoder drawPrimitives: MTLPrimitiveTypeTriangleStrip vertexStart: 0 vertexCount: 4 instanceCount: 1 baseInstance: 0];

    [encoder endEncoding];
}

beforeRendering()에 연결된 슬롯인 render()는 sync()에서 생성된 버퍼와 파이프라인 상태 객체를 사용하여 렌더링 명령을 인코딩합니다.

예제 프로젝트 @ 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.