Procedural Texture Example#
Demonstrates how to provide custom texture data from Python.
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.
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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 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>