Procedural Texture Example

Demonstrates how to provide custom texture data from Python.

QtQuick3D Procedural Texture Example

In this example, we leverage QQuick3DTextureData and the textureData property of Texture to produce texture data dynamically from Python, rather than sourcing it from a static asset.

Download this example

# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine

from gradienttexture import GradientTexture  # noqa: F401

from pathlib import Path

import os
import sys

if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    app.setOrganizationName("QtProject")
    app.setApplicationName("ProceduralTexture")

    engine = QQmlApplicationEngine()
    app_dir = Path(__file__).parent
    engine.addImportPath(os.fspath(app_dir))
    engine.loadFromModule("ProceduralTextureModule", "Main")

    if not engine.rootObjects():
        sys.exit(-1)

    ex = app.exec()
    del engine

    sys.exit(ex)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import Signal, Property, QSize
from PySide6.QtGui import QColor
from PySide6.QtQuick3D import QQuick3DTextureData
from PySide6.QtQml import QmlElement

QML_IMPORT_NAME = "ProceduralTextureModule"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class GradientTexture(QQuick3DTextureData):

    heightChanged = Signal(int)
    widthChanged = Signal(int)
    startColorChanged = Signal(QColor)
    endColorChanged = Signal(QColor)

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self._height = 256
        self._width = 256
        self._startcolor = QColor("#d4fc79")
        self._endcolor = QColor("#96e6a1")
        self.updateTexture()

    @Property(int, notify=heightChanged)
    def height(self):
        return self._height

    @height.setter
    def height(self, val):
        if self._height == val:
            return
        self._height = val
        self.updateTexture()
        self.heightChanged.emit(self._height)

    @Property(int, notify=widthChanged)
    def width(self):
        return self._width

    @width.setter
    def width(self, val):
        if self._width == val:
            return
        self._width = val
        self.updateTexture()
        self.widthChanged.emit(self._width)

    @Property(QColor, notify=startColorChanged)
    def startColor(self):
        return self._startcolor

    @startColor.setter
    def startColor(self, val):
        if self._startcolor == val:
            return
        self._startcolor = val
        self.updateTexture()
        self.startColorChanged.emit(self._startcolor)

    @Property(QColor, notify=endColorChanged)
    def endColor(self):
        return self._endcolor

    @endColor.setter
    def endColor(self, val):
        if self._endcolor == val:
            return
        self._endcolor = val
        self.updateTexture()
        self.endColorChanged.emit(self._endcolor)

    def updateTexture(self):
        self.setSize(QSize(self._width, self._height))
        self.setFormat(QQuick3DTextureData.RGBA8)
        self.setHasTransparency(False)
        self.setTextureData(self.generate_texture())

    def generate_texture(self):
        # Generate a horizontal gradient by interpolating between start and end colors.
        gradientScanline = [
            self.linear_interpolate(self._startcolor, self._endcolor, x / self._width)
            for x in range(self._width)
        ]
        # Convert the gradient colors to a flattened list of RGBA values.
        flattenedGradient = [
            component
            for color in gradientScanline
            for component in (color.red(), color.green(), color.blue(), 255)
        ]
        # Repeat the gradient vertically to form the texture.
        return bytearray(flattenedGradient * self._height)

    def linear_interpolate(self, color1, color2, value):
        output = QColor()

        output.setRedF(color1.redF() + (value * (color2.redF() - color1.redF())))
        output.setGreenF(color1.greenF() + (value * (color2.greenF() - color1.greenF())))
        output.setBlueF(color1.blueF() + (value * (color2.blueF() - color1.blueF())))

        return output
module ProceduralTextureModule
Main 1.0 Main.qml
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick3D
import QtQuick3D.Helpers
import QtQuick.Controls
import QtQuick.Layouts

import ProceduralTextureModule

ApplicationWindow {
    id: window
    width: 480
    height: 320
    visible: true
    title: "Procedural Texture Example"

    QtObject {
        id: applicationState
        property int size: size256.checked ? 256 : 16
        property color startColor: "#00dbde"
        property color endColor: "#fc00ff"
        property int filterMode: size === 256 ? Texture.Linear : Texture.Nearest
        property Texture texture: pythonModeRadio.checked ? textureFromPython : textureFromQML

        function randomColor() : color {
            return Qt.rgba(Math.random(),
                           Math.random(),
                           Math.random(),
                           1.0);
        }
    }

    View3D {
        anchors.fill: parent

        DirectionalLight {
        }

        PerspectiveCamera {
            z: 300
        }

        Texture {
            id: textureFromPython

            minFilter: applicationState.filterMode
            magFilter: applicationState.filterMode
            textureData: gradientTexture

            GradientTexture {
                id: gradientTexture
                startColor: applicationState.startColor
                endColor: applicationState.endColor
                width: applicationState.size
                height: width
            }
        }

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

        Model {
            source: "#Cube"

            materials: [
                PrincipledMaterial {
                    baseColorMap: applicationState.texture
                }
            ]

            PropertyAnimation on eulerRotation.y {
                from: 0
                to: 360
                duration: 5000
                loops: Animation.Infinite
                running: true
            }
        }
    }

    Pane {
        ColumnLayout {

            GroupBox {
                title: "Size:"

                ButtonGroup  {
                    id: sizeGroup
                }

                ColumnLayout {
                    RadioButton {
                        id: size256
                        text: "256x256"
                        checked: true
                        ButtonGroup.group: sizeGroup
                    }
                    RadioButton {
                        id: size512
                        text: "16x16"
                        checked: false
                        ButtonGroup.group: sizeGroup
                    }
                }
            }

            GroupBox {
                title: "Backend:"

                ButtonGroup {
                    id: backendGroup
                }

                ColumnLayout {
                    RadioButton {
                        id: pythonModeRadio
                        text: "Python"
                        checked: true
                        ButtonGroup.group: backendGroup
                    }
                    RadioButton {
                        id: qmlModeRadio
                        text: "QML"
                        checked: false
                        ButtonGroup.group: backendGroup
                    }
                }

            }

            Button {
                text: "Random Start Color"
                onClicked: applicationState.startColor = applicationState.randomColor();
            }
            Button {
                text: "Random End Color"
                onClicked: applicationState.endColor = applicationState.randomColor();
            }
        }
    }
}
<RCC>
    <qresource prefix="/qt/qml/ProceduralTextureModule">
        <file>qmldir</file>
        <file>Main.qml</file>
    </qresource>
</RCC>