シーングラフ - カスタムマテリアル
Qt Quick のシーングラフでカスタムマテリアルを実装する方法を説明します。
このカスタムマテリアルの例では、カスタム頂点シェーダーおよびフラグメントシェーダーを持つマテリアルを使用してレンダリングされるアイテムを実装する方法を示します。

シェーダーとマテリアル
主な機能はフラグメントシェーダーにあります
// 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に公開したいユニフォームに対応する3つのプロパティが追加されています。
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 アイテムと同様に、実装は2つに分かれています。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 ¢er)
{
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() の再実装において、アイテムからシーングラフへコピーされます。この関数が呼び出される際、2つのスレッドは同期ポイントにあるため、両方のクラスに安全にアクセスできます。
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;
}例の続き
このアプリケーションは、QGuiApplication とQQuickView を持ち、.qmlファイルを渡す、シンプルな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
}
}例をもう少し面白くするために、ズームレベルと反復回数の制限を変更するアニメーションを追加します。中心位置は一定のままです。
© 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.