Qt Quick 3D - 프로시저럴 텍스처 예제
C++ 또는 QML에서 커스텀 텍스처 데이터를 제공하는 방법을 보여줍니다.

정적 에셋에서 로드하는 대신 런타임에 동적으로 생성된 텍스처 데이터를 제공하는 다양한 방법을 보여줍니다. 데모를 위해 이 예제에서는 제공된 시작 및 끝 색상에서 수직 그라데이션 텍스처를 생성합니다.
먼저 텍스처 데이터에 대한 C++ 클래스를 정의합니다. QQuick3DTextureData 의 서브클래스로 만듭니다. 가상 함수가 없기 때문에 꼭 필요한 것은 아니지만 모든 것을 하나의 클래스에 넣는 것이 훨씬 편리합니다. 사용할 프로퍼티를 정의하고 QML_NAMED_ELEMENT 을 추가하여 QML에서 사용할 수 있도록 합니다:
class GradientTexture : public QQuick3DTextureData { Q_OBJECT Q_PROPERTY(int height READ height WRITE setHeight NOTIFY heightChanged) Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged) Q_PROPERTY(QColor startColor READ startColor WRITE setStartColor NOTIFY startColorChanged) Q_PROPERTY(QColor endColor READ endColor WRITE setEndColor NOTIFY endColorChanged) QML_NAMED_ELEMENT(GradientTexture) ...
텍스처를 업데이트하는 함수를 추가합니다. 텍스처를 구성하는 데는 setSize와 setFormat을 사용하고 이미지 데이터를 설정하는 데는 setTextureData를 사용합니다:
void GradientTexture::updateTexture() { setSize(QSize(m_width, m_height)); setFormat(QQuick3DTextureData::RGBA8); setHasTransparency(false); setTextureData(generateTexture()); }
generateTexture 함수는 올바른 크기의 QByteArray 을 생성하고 이미지 데이터로 채웁니다:
QByteArray GradientTexture::generateTexture() { QByteArray imageData; // Create a horizontal gradient between startColor and endColor // Create a single scanline and reuse that data for each QByteArray gradientScanline; gradientScanline.resize(m_width * 4); // RGBA8 for (int x = 0; x < m_width; ++x) { QColor color = linearInterpolate(m_startColor, m_endColor, x / float(m_width)); int offset = x * 4; gradientScanline.data()[offset + 0] = char(color.red()); gradientScanline.data()[offset + 1] = char(color.green()); gradientScanline.data()[offset + 2] = char(color.blue()); gradientScanline.data()[offset + 3] = char(255); } for (int y = 0; y < m_height; ++y) imageData += gradientScanline; return imageData; }
프로퍼티가 변경될 때마다 updateTexture 함수를 호출합니다:
void GradientTexture::setStartColor(QColor startColor) { if (m_startColor == startColor) return; m_startColor = startColor; emit startColorChanged(m_startColor); updateTexture(); }
마지막으로 QML에서 새 텍스처를 사용할 수 있습니다:
Texture { id: textureFromCpp minFilter: applicationState.filterMode magFilter: applicationState.filterMode textureData: gradientTexture GradientTexture { id: gradientTexture startColor: applicationState.startColor endColor: applicationState.endColor width: applicationState.size height: width } }
QML에서도 동일한 텍스처 데이터를 생성할 수 있습니다. 이 경우 ProceduralTextureData 컴포넌트를 사용합니다:
Texture { id: textureFromQML minFilter: applicationState.filterMode magFilter: applicationState.filterMode textureData: gradientTextureDataQML ProceduralTextureData { id: gradientTextureDataQML property color startColor: applicationState.startColor property color endColor: applicationState.endColor width: applicationState.size height: width textureData: generateTextureData() function linearInterpolate(startColor : color, endColor : color, fraction : real) : color{ return Qt.rgba( startColor.r + (endColor.r - startColor.r) * fraction, startColor.g + (endColor.g - startColor.g) * fraction, startColor.b + (endColor.b - startColor.b) * fraction, startColor.a + (endColor.a - startColor.a) * fraction ); } function generateTextureData() { let dataBuffer = new ArrayBuffer(width * height * 4) let data = new Uint8Array(dataBuffer) let gradientScanline = new Uint8Array(width * 4); for (let x = 0; x < width; ++x) { let color = linearInterpolate(startColor, endColor, x / width); let offset = x * 4; gradientScanline[offset + 0] = color.r * 255; gradientScanline[offset + 1] = color.g * 255; gradientScanline[offset + 2] = color.b * 255; gradientScanline[offset + 3] = color.a * 255; } for (let y = 0; y < height; ++y) { data.set(gradientScanline, y * width * 4); } return dataBuffer; } } }
C++에서와 마찬가지로 텍스처의 크기와 형식을 반영하는 이미지 데이터로 QByteArray 를 채웁니다. QML에서 이 작업을 수행할 때는 불필요한 유형 변환을 피하기 위해 ArrayBuffer 유형을 사용합니다.
앞의 두 예제에서는 텍스처에 대한 모든 데이터가 CPU 측에서 생성된 다음 텍스처 데이터로 사용하기 위해 GPU에 업로드되었습니다. 텍스처 데이터를 GPU에서 직접 생성하는 것도 가능합니다. 이 경우 대신 QQuick3DTextureProviderExtension 의 서브클래스를 사용합니다.
class GradientTextureProvider : public QQuick3DTextureProviderExtension { Q_OBJECT Q_PROPERTY(int height READ height WRITE setHeight NOTIFY heightChanged) Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged) Q_PROPERTY(QColor startColor READ startColor WRITE setStartColor NOTIFY startColorChanged) Q_PROPERTY(QColor endColor READ endColor WRITE setEndColor NOTIFY endColorChanged) QML_ELEMENT
QQuick3DTextureProviderExtension 기반 클래스는 Qt RHI API를 사용하여 렌더링 확장을 정의합니다. 서브클래스는 QQuick3DTextureProviderExtension::updateSpatialNode()이 호출될 때 QSSGRenderExtension 기반 객체를 반환해야 합니다.
QSSGRenderGraphObject *GradientTextureProvider::updateSpatialNode(QSSGRenderGraphObject *node) { if (!node) node = new GradientTextureProviderNode(this); // Update the state of the backend node auto gradientNode = static_cast<GradientTextureProviderNode *>(node); gradientNode->m_isDirty = true; gradientNode->m_width = m_width; gradientNode->m_height = m_height; gradientNode->m_startColor = m_startColor; gradientNode->m_endColor = m_endColor; return node; }
실제로 렌더링되는 것은 이 QSSGRenderExtension 서브클래스에 의해 정의됩니다:
class GradientTextureProviderNode : public QSSGRenderTextureProviderExtension { public: explicit GradientTextureProviderNode(GradientTextureProvider *ext); ~GradientTextureProviderNode() override; bool prepareData(QSSGFrameData &data) override; void prepareRender(QSSGFrameData &data) override; void render(QSSGFrameData &data) override; void resetForFrame() override; bool m_isDirty = false; // state int m_width = 256; int m_height = 256; QColor m_startColor = QColor(Qt::red); QColor m_endColor = QColor(Qt::blue); private: QPointer<GradientTextureProvider> m_ext; QSSGExtensionId extensionId {}; std::unique_ptr<QRhiBuffer> quadGeometryVertexBuffer; std::unique_ptr<QRhiBuffer> quadGeometryIndexBuffer; // std::unique_ptr<QRhiTexture> outputTexture; // the final output texture std::unique_ptr<QRhiTextureRenderTarget> outputTextureRenderTarget; std::unique_ptr<QRhiRenderPassDescriptor> ouputTextureRenderPassDescriptor; std::unique_ptr<QRhiBuffer> gradientTextureUniformBuffer; std::unique_ptr<QRhiShaderResourceBindings> gradientTextureShaderResouceBindings; std::unique_ptr<QRhiGraphicsPipeline> gradientTexture2dPipeline; };
QSSGRenderExtension 의 인터페이스를 구현함으로써 확장은 텍스처 데이터를 GPU에서 직접 렌더링할 수 있습니다. prepareDate 구현은 출력 텍스처가 올바른 크기와 형식으로 생성되었는지 확인하고 출력 텍스처를 제공 중인 텍스처로 등록합니다.
bool GradientTextureProviderNode::prepareData(QSSGFrameData &data) { if (!m_isDirty) return false; const auto &ctxIfx = data.contextInterface(); const auto &rhiCtx = ctxIfx->rhiContext(); QRhi *rhi = rhiCtx->rhi(); // If there is no available rhi context, then we can't create the texture if (!rhiCtx) return false; extensionId = m_ext ? QQuick3DExtensionHelpers::getExtensionId(*m_ext) : QSSGExtensionId{}; if (QQuick3DExtensionHelpers::isNull(extensionId)) return false; // Make sure that the output texture is created and registered as the texture provider if (!outputTexture || outputTexture->pixelSize().width() != m_width || outputTexture->pixelSize().height() != m_height) { outputTexture.reset(rhi->newTexture(QRhiTexture::Format::RGBA8, QSize(m_width, m_height), 1, QRhiTexture::RenderTarget | QRhiTexture::sRGB)); outputTexture->create(); outputTextureRenderTarget.reset(rhi->newTextureRenderTarget({ outputTexture.get() })); ouputTextureRenderPassDescriptor.reset(outputTextureRenderTarget->newCompatibleRenderPassDescriptor()); outputTextureRenderTarget->setRenderPassDescriptor(ouputTextureRenderPassDescriptor.get()); outputTextureRenderTarget->create(); // Register the output as the texture provider QSSGRenderExtensionHelpers::registerRenderResult(data, extensionId, outputTexture.get()); gradientTexture2dPipeline.reset(); } // If m_isDirty is true than prepareRender and render will actually get called. return m_isDirty; }
prepareRender는 파이프라인이 생성되고 균일 버퍼가 업데이트되는 곳입니다.
void GradientTextureProviderNode::prepareRender(QSSGFrameData &data) { const auto &ctxIfx = data.contextInterface(); const auto &rhiCtx = ctxIfx->rhiContext(); if (!rhiCtx) return; QRhi *rhi = rhiCtx->rhi(); QRhiCommandBuffer *cb = rhiCtx->commandBuffer(); QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch(); // Create the pipeline if necessary if (!gradientTexture2dPipeline) { // 1 quad (2 trianges), pos + uv. 4 vertices, 5 values each (x, y, z, u, v) quadGeometryVertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, 5 * 4 * sizeof(float))); quadGeometryVertexBuffer->create(); // 6 indexes (2 triangles) quadGeometryIndexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, 6 * sizeof(uint16_t))); quadGeometryIndexBuffer->create(); // Uniform buffer: packed into 2 * vec4 (2 RGBA colors) const size_t uBufSize = (sizeof(float) * 4) * 2; gradientTextureUniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, uBufSize)); gradientTextureUniformBuffer->create(); // Uniform buffer is only bound/used in Fragment Shader gradientTextureShaderResouceBindings.reset(rhi->newShaderResourceBindings()); gradientTextureShaderResouceBindings->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::FragmentStage, gradientTextureUniformBuffer.get()), }); gradientTextureShaderResouceBindings->create(); gradientTexture2dPipeline.reset(rhi->newGraphicsPipeline()); gradientTexture2dPipeline->setShaderStages({ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shaders/gradient.vert.qsb")) }, { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shaders/gradient.frag.qsb")) } }); // 2 Attributes, Position (vec3) + UV (vec2) QRhiVertexInputLayout inputLayout; inputLayout.setBindings({ { 5 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 }, { 0, 1, QRhiVertexInputAttribute::Float2, 3 * sizeof(float) } }); gradientTexture2dPipeline->setVertexInputLayout(inputLayout); gradientTexture2dPipeline->setShaderResourceBindings(gradientTextureShaderResouceBindings.get()); gradientTexture2dPipeline->setRenderPassDescriptor(ouputTextureRenderPassDescriptor.get()); gradientTexture2dPipeline->create(); // Upload the static quad geometry part resourceUpdates->uploadStaticBuffer(quadGeometryVertexBuffer.get(), g_vertexData); resourceUpdates->uploadStaticBuffer(quadGeometryIndexBuffer.get(), g_indexData); } // Upload the uniform buffer data const float colorData[8] = { m_startColor.redF(), m_startColor.greenF(), m_startColor.blueF(), m_startColor.alphaF(), m_endColor.redF(), m_endColor.greenF(), m_endColor.blueF(), m_endColor.alphaF() }; resourceUpdates->updateDynamicBuffer(gradientTextureUniformBuffer.get(), 0, sizeof(colorData), colorData); cb->resourceUpdate(resourceUpdates); m_isDirty = false; }
패스에 대한 실제 렌더링은 렌더 함수에서 정의됩니다.
void GradientTextureProviderNode::render(QSSGFrameData &data) { const auto &ctxIfx = data.contextInterface(); const auto &rhiCtx = ctxIfx->rhiContext(); if (!rhiCtx) return; QRhiCommandBuffer *cb = rhiCtx->commandBuffer(); // Render the quad with our pipeline to the outputTexture cb->beginPass(outputTextureRenderTarget.get(), Qt::black, { 1.0f, 0 }, nullptr, rhiCtx->commonPassFlags()); cb->setViewport(QRhiViewport(0, 0, m_width, m_height)); cb->setGraphicsPipeline(gradientTexture2dPipeline.get()); cb->setShaderResources(gradientTextureShaderResouceBindings.get()); QRhiCommandBuffer::VertexInput vb(quadGeometryVertexBuffer.get(), 0); cb->setVertexInput(0, 1, &vb, quadGeometryIndexBuffer.get(), QRhiCommandBuffer::IndexFormat::IndexUInt16); cb->drawIndexed(6); cb->endPass(); }
이 예제에서는 QML의 텍스처 프로바이더를 다음과 같이 사용합니다:
Texture { id: textureFromGPU minFilter: applicationState.filterMode magFilter: applicationState.filterMode textureProvider: GradientTextureProvider { startColor: applicationState.startColor endColor: applicationState.endColor width: applicationState.size height: width } }
© 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.