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

Download this example

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

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
        )
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

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,
            )
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause


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())
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

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>