Custom Geometry Example

This example makes use of QQuick3DGeometry and the geometry property of Model to render a mesh with vertex, normal, and texture coordinates specified from Python instead of a pre-baked asset.

In addition, the GridGeometry is also demonstrated. GridGeometry is a built-in QQuick3DGeometry implementation that provides a mesh with line primitives suitable for displaying a grid.

The focus on this example will be on the code that provides the custom geometry.

QtQuick3D Custom Geometry Screenshot
import random

import numpy as np
from PySide6.QtGui import QVector3D
from PySide6.QtQml import QmlElement
from PySide6.QtQuick3D import QQuick3DGeometry

QML_IMPORT_NAME = "ExamplePointGeometry"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class ExamplePointGeometry(QQuick3DGeometry):
    def __init__(self, parent=None):
        QQuick3DGeometry.__init__(self, parent)
        self.updateData()

    def updateData(self):
        self.clear()

        # We use numpy arrays to handle the vertex data,
        # but still we need to consider the 'sizeof(float)'
        # from C to set the Stride, and Attributes for the
        # underlying Qt methods
        FLOAT_SIZE = 4
        NUM_POINTS = 2000
        stride = 3

        vertexData = np.zeros(NUM_POINTS * stride, dtype=np.float32)

        p = 0
        for i in range(NUM_POINTS):
            vertexData[p] = random.uniform(-5.0, +5.0)
            p += 1
            vertexData[p] = random.uniform(-5.0, +5.0)
            p += 1
            vertexData[p] = 0.0
            p += 1

        self.setVertexData(vertexData.tobytes())
        self.setStride(stride * FLOAT_SIZE)
        self.setBounds(QVector3D(-5.0, -5.0, 0.0), QVector3D(+5.0, +5.0, 0.0))

        self.setPrimitiveType(QQuick3DGeometry.PrimitiveType.Points)

        self.addAttribute(
            QQuick3DGeometry.Attribute.PositionSemantic, 0, QQuick3DGeometry.Attribute.F32Type
        )
import numpy as np
from PySide6.QtCore import Property, Signal
from PySide6.QtGui import QVector3D
from PySide6.QtQml import QmlElement
from PySide6.QtQuick3D import QQuick3DGeometry

QML_IMPORT_NAME = "ExampleTriangleGeometry"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class ExampleTriangleGeometry(QQuick3DGeometry):

    normalsChanged = Signal()
    normalXYChanged = Signal()
    uvChanged = Signal()
    uvAdjustChanged = Signal()

    def __init__(self, parent=None):
        QQuick3DGeometry.__init__(self, parent)
        self._hasNormals = False
        self._normalXY = 0.0
        self._hasUV = False
        self._uvAdjust = 0.0

        self.updateData()

    @Property(bool, notify=normalsChanged)
    def normals(self):
        return self._hasNormals

    @normals.setter
    def normals(self, enable):
        if self._hasNormals == enable:
            return

        self._hasNormals = enable
        self.normalsChanged.emit()
        self.updateData()
        self.update()

    @Property(float, notify=normalXYChanged)
    def normalXY(self):
        return self._normalXY

    @normalXY.setter
    def normalXY(self, xy):
        if self._normalXY == xy:
            return

        self._normalXY = xy
        self.normalXYChanged.emit()
        self.updateData()
        self.update()

    @Property(bool, notify=uvChanged)
    def uv(self):
        return self._hasUV

    @uv.setter
    def uv(self, enable):
        if self._hasUV == enable:
            return

        self._hasUV = enable
        self.uvChanged.emit()
        self.updateData()
        self.update()

    @Property(float, notify=uvAdjustChanged)
    def uvAdjust(self):
        return self._uvAdjust

    @uvAdjust.setter
    def uvAdjust(self, f):
        if self._uvAdjust == f:
            return

        self._uvAdjust = f
        self.uvAdjustChanged.emit()
        self.updateData()
        self.update()

    def updateData(self):
        self.clear()

        stride = 3
        if self._hasNormals:
            stride += 3
        if self._hasUV:
            stride += 2

        # We use numpy arrays to handle the vertex data,
        # but still we need to consider the 'sizeof(float)'
        # from C to set the Stride, and Attributes for the
        # underlying Qt methods
        FLOAT_SIZE = 4
        vertexData = np.zeros(3 * stride, dtype=np.float32)

        # a triangle, front face = counter-clockwise
        p = 0
        vertexData[p] = -1.0
        p += 1
        vertexData[p] = -1.0
        p += 1
        vertexData[p] = 0.0
        p += 1

        if self._hasNormals:
            vertexData[p] = self._normalXY
            p += 1
            vertexData[p] = self._normalXY
            p += 1
            vertexData[p] = 1.0
            p += 1

        if self._hasUV:
            vertexData[p] = 0.0 + self._uvAdjust
            p += 1
            vertexData[p] = 0.0 + self._uvAdjust
            p += 1

        vertexData[p] = 1.0
        p += 1
        vertexData[p] = -1.0
        p += 1
        vertexData[p] = 0.0
        p += 1

        if self._hasNormals:
            vertexData[p] = self._normalXY
            p += 1
            vertexData[p] = self._normalXY
            p += 1
            vertexData[p] = 1.0
            p += 1

        if self._hasUV:
            vertexData[p] = 1.0 - self._uvAdjust
            p += 1
            vertexData[p] = 0.0 + self._uvAdjust
            p += 1

        vertexData[p] = 0.0
        p += 1
        vertexData[p] = 1.0
        p += 1
        vertexData[p] = 0.0
        p += 1

        if self._hasNormals:
            vertexData[p] = self._normalXY
            p += 1
            vertexData[p] = self._normalXY
            p += 1
            vertexData[p] = 1.0
            p += 1

        if self._hasUV:
            vertexData[p] = 1.0 - self._uvAdjust
            p += 1
            vertexData[p] = 1.0 - self._uvAdjust
            p += 1

        self.setVertexData(vertexData.tobytes())
        self.setStride(stride * FLOAT_SIZE)
        self.setBounds(QVector3D(-1.0, -1.0, 0.0), QVector3D(+1.0, +1.0, 0.0))
        self.setPrimitiveType(QQuick3DGeometry.PrimitiveType.Triangles)
        self.addAttribute(
            QQuick3DGeometry.Attribute.PositionSemantic, 0, QQuick3DGeometry.Attribute.F32Type
        )

        if self._hasNormals:
            self.addAttribute(
                QQuick3DGeometry.Attribute.NormalSemantic,
                3 * FLOAT_SIZE,
                QQuick3DGeometry.Attribute.F32Type,
            )

        if self._hasUV:
            self.addAttribute(
                QQuick3DGeometry.Attribute.TexCoordSemantic,
                6 * FLOAT_SIZE if self._hasNormals else 3 * FLOAT_SIZE,
                QQuick3DGeometry.Attribute.F32Type,
            )
import os
import sys

from PySide6.QtCore import QUrl
from PySide6.QtGui import QGuiApplication, QSurfaceFormat
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuick3D import QQuick3D

# Imports to trigger the resources and registration of QML elements
import resources_rc
from examplepoint import ExamplePointGeometry
from exampletriangle import ExampleTriangleGeometry

if __name__ == "__main__":
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Basic"
    app = QGuiApplication(sys.argv)

    QSurfaceFormat.setDefaultFormat(QQuick3D.idealSurfaceFormat())

    engine = QQmlApplicationEngine()
    engine.load(QUrl.fromLocalFile(":/main.qml"))
    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec())
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick3D
import QtQuick3D.Helpers
import ExamplePointGeometry
import ExampleTriangleGeometry


Window {
    id: window
    width: 1280
    height: 720
    visible: true
    color: "#848895"

    View3D {
        id: v3d
        anchors.fill: parent
        camera: camera

        PerspectiveCamera {
            id: camera
            position: Qt.vector3d(0, 0, 600)
        }

        DirectionalLight {
            position: Qt.vector3d(-500, 500, -100)
            color: Qt.rgba(0.4, 0.2, 0.6, 1.0)
            ambientColor: Qt.rgba(0.1, 0.1, 0.1, 1.0)
        }

        PointLight {
            position: Qt.vector3d(0, 0, 100)
            color: Qt.rgba(0.1, 1.0, 0.1, 1.0)
            ambientColor: Qt.rgba(0.2, 0.2, 0.2, 1.0)
        }

        Model {
            visible: radioGridGeom.checked
            scale: Qt.vector3d(100, 100, 100)
            geometry: GridGeometry {
                id: grid
                horizontalLines: 20
                verticalLines: 20
            }
            materials: [
                DefaultMaterial {
                    lineWidth: sliderLineWidth.value
                }
            ]
        }

        //! [model triangle]
        Model {
            visible: radioCustGeom.checked
            scale: Qt.vector3d(100, 100, 100)
            geometry: ExampleTriangleGeometry {
                normals: cbNorm.checked
                normalXY: sliderNorm.value
                uv: cbUV.checked
                uvAdjust: sliderUV.value
            }
            materials: [
                DefaultMaterial {
                    Texture {
                        id: baseColorMap
                        source: "qt_logo_rect.png"
                    }
                    cullMode: DefaultMaterial.NoCulling
                    diffuseMap: cbTexture.checked ? baseColorMap : null
                    specularAmount: 0.5
                }
            ]
        }
        //! [model triangle]

        Model {
            visible: radioPointGeom.checked
            scale: Qt.vector3d(100, 100, 100)
            geometry: ExamplePointGeometry { }
            materials: [
                DefaultMaterial {
                    lighting: DefaultMaterial.NoLighting
                    cullMode: DefaultMaterial.NoCulling
                    diffuseColor: "yellow"
                    pointSize: sliderPointSize.value
                }
            ]
        }
    }

    WasdController {
        controlledObject: camera
    }

    ColumnLayout {
        Label {
            text: "Use WASD and mouse to navigate"
            font.bold: true
        }
        ButtonGroup {
            buttons: [ radioGridGeom, radioCustGeom, radioPointGeom ]
        }
        RadioButton {
            id: radioGridGeom
            text: "GridGeometry"
            checked: true
            focusPolicy: Qt.NoFocus
        }
        RadioButton {
            id: radioCustGeom
            text: "Custom geometry from application (triangle)"
            checked: false
            focusPolicy: Qt.NoFocus
        }
        RadioButton {
            id: radioPointGeom
            text: "Custom geometry from application (points)"
            checked: false
            focusPolicy: Qt.NoFocus
        }
        RowLayout {
            visible: radioGridGeom.checked
            ColumnLayout {
                Button {
                    text: "More X cells"
                    onClicked: grid.verticalLines += 1
                    focusPolicy: Qt.NoFocus
                }
                Button  {
                    text: "Fewer X cells"
                    onClicked: grid.verticalLines -= 1
                    focusPolicy: Qt.NoFocus
                }
            }
            ColumnLayout {
                Button {
                    text: "More Y cells"
                    onClicked: grid.horizontalLines += 1
                    focusPolicy: Qt.NoFocus
                }
                Button  {
                    text: "Fewer Y cells"
                    onClicked: grid.horizontalLines -= 1
                    focusPolicy: Qt.NoFocus
                }
            }
        }
        RowLayout {
            visible: radioGridGeom.checked
            Label {
                text: "Line width (if supported)"
            }
            Slider {
                id: sliderLineWidth
                from: 1.0
                to: 10.0
                stepSize: 0.5
                value: 1.0
                focusPolicy: Qt.NoFocus
            }
        }
        RowLayout {
            visible: radioCustGeom.checked
            CheckBox {
                id: cbNorm
                text: "provide normals in geometry"
                checked: false
                focusPolicy: Qt.NoFocus
            }
            RowLayout {
                Label {
                    text: "manual adjust"
                }
                Slider {
                    id: sliderNorm
                    from: 0.0
                    to: 1.0
                    stepSize: 0.01
                    value: 0.0
                    focusPolicy: Qt.NoFocus
                }
            }
        }
        RowLayout {
            visible: radioCustGeom.checked
            CheckBox {
                id: cbTexture
                text: "enable base color map"
                checked: false
                focusPolicy: Qt.NoFocus
            }
            CheckBox {
                id: cbUV
                text: "provide UV in geometry"
                checked: false
                focusPolicy: Qt.NoFocus
            }
            RowLayout {
                Label {
                    text: "UV adjust"
                }
                Slider {
                    id: sliderUV
                    from: 0.0
                    to: 1.0
                    stepSize: 0.01
                    value: 0.0
                    focusPolicy: Qt.NoFocus
                }
            }
        }
        RowLayout {
            visible: radioPointGeom.checked
            ColumnLayout {
                RowLayout {
                    Label {
                        text: "Point size (if supported)"
                    }
                    Slider {
                        id: sliderPointSize
                        from: 1.0
                        to: 16.0
                        stepSize: 1.0
                        value: 1.0
                        focusPolicy: Qt.NoFocus
                    }
                }
            }
        }
        TextArea {
            id: infoText
            readOnly: true
        }
    }
}
<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
        <file>qt_logo_rect.png</file>
    </qresource>
</RCC>