씬 그래프 - 메탈 텍스처 임포트

메탈로 직접 만든 텍스처를 사용하는 방법을 보여줍니다.

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

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
    }

이 애플리케이션은 커스텀 텍스처 항목이라는 이름으로 커스텀 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 Quicks 시나리오 그래프의 명령 버퍼에 인코딩하여 Metal 텍스처의 내용을 업데이트합니다. Qt Quick 가 자체 렌더링 명령을 인코딩하기 시작하기 전에 신호가 방출되므로 beforeRendering()이 이에 적합한 곳입니다. 대신 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 래핑(소유가 아닌)이 생성됩니다. 마지막으로, QSGTexture 은 기본 클래스의 setTexture() 함수를 호출하여 기본 머티리얼과 연결됩니다.

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

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

예제 프로젝트 @ code.qt.io

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