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
from __future__ import annotations

import random

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

QML_IMPORT_NAME = "CustomGeometryExample"
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.Semantic.PositionSemantic, 0,
            QQuick3DGeometry.Attribute.ComponentType.F32Type
        )
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

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 = "CustomGeometryExample"
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.Semantic.PositionSemantic, 0,
            QQuick3DGeometry.Attribute.ComponentType.F32Type
        )

        if self._hasNormals:
            self.addAttribute(
                QQuick3DGeometry.Attribute.Semantic.NormalSemantic,
                3 * FLOAT_SIZE,
                QQuick3DGeometry.Attribute.ComponentType.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
from __future__ import annotations


import sys
from pathlib import Path

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

from examplepoint import ExamplePointGeometry  # noqa: F401
from exampletriangle import ExampleTriangleGeometry  # noqa: F401

if __name__ == "__main__":
    app = QGuiApplication(sys.argv)

    QSurfaceFormat.setDefaultFormat(QQuick3D.idealSurfaceFormat())

    engine = QQmlApplicationEngine()
    engine.addImportPath(Path(__file__).parent)
    engine.loadFromModule("CustomGeometryExample", "Main")
    if not engine.rootObjects():
        sys.exit(-1)

    exit_code = app.exec()
    del engine
    sys.exit(exit_code)
// Copyright (C) 2023 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 CustomGeometryExample

ApplicationWindow {
    id: window
    width: 1280
    height: 720
    visible: true
    title: "Custom Geometry Example"

    property bool isLandscape: width > height

    View3D {
        id: v3d
        anchors.left: window.isLandscape ? controlsPane.right : parent.left
        anchors.top: window.isLandscape ? parent.top : controlsPane.bottom
        anchors.right: parent.right
        anchors.bottom: parent.bottom

        camera: camera

        environment: SceneEnvironment {
            id: env
            backgroundMode: SceneEnvironment.Color
            clearColor: "#002b36"
        }

        Node {
            id: originNode
            PerspectiveCamera {
                id: cameraNode
                z: 600
            }
        }

        DirectionalLight {
            id: directionalLight
            color: Qt.rgba(0.4, 0.2, 0.6, 1.0)
            ambientColor: Qt.rgba(0.1, 0.1, 0.1, 1.0)
        }

        PointLight {
            id: 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 {
            id: gridModel
            visible: false
            scale: Qt.vector3d(100, 100, 100)
            geometry: GridGeometry {
                id: grid
                horizontalLines: 20
                verticalLines: 20
            }
            materials: [
                PrincipledMaterial {
                    lineWidth: sliderLineWidth.value
                }
            ]
        }

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

        Model {
            id: pointModel
            visible: false
            scale: Qt.vector3d(100, 100, 100)
            geometry: ExamplePointGeometry { }
            materials: [
                PrincipledMaterial {
                    lighting: PrincipledMaterial.NoLighting
                    cullMode: PrincipledMaterial.NoCulling
                    baseColor: "yellow"
                    pointSize: sliderPointSize.value
                }
            ]
        }

        Model {
            id: torusModel
            visible: false
            geometry: TorusMesh {
                radius: radiusSlider.value
                tubeRadius: tubeRadiusSlider.value
                segments: segmentsSlider.value
                rings: ringsSlider.value
            }
            materials: [
                PrincipledMaterial {
                    id: torusMaterial
                    baseColor: "#dc322f"
                    metalness: 0.0
                    roughness: 0.1
                }
            ]
        }

        OrbitCameraController {
            origin: originNode
            camera: cameraNode
        }
    }

    Pane {
        id: controlsPane
        width: window.isLandscape ? implicitWidth : window.width
        height: window.isLandscape ? window.height : implicitHeight
        ColumnLayout {
            GroupBox {
                title: "Mode"
                ButtonGroup {
                    id: modeGroup
                    buttons: [ radioGridGeom, radioCustGeom, radioPointGeom, radioQMLGeom ]
                }
                ColumnLayout {
                    RadioButton {
                        id: radioGridGeom
                        text: "GridGeometry"
                        checked: true
                    }
                    RadioButton {
                        id: radioCustGeom
                        text: "Custom geometry from application (triangle)"
                        checked: false
                    }
                    RadioButton {
                        id: radioPointGeom
                        text: "Custom geometry from application (points)"
                        checked: false
                    }
                    RadioButton {
                        id: radioQMLGeom
                        text: "Custom geometry from QML"
                        checked: false
                    }
                }
            }

            Pane {
                id: gridSettings
                visible: false
                ColumnLayout {
                    Button {
                        text: "+ Y Cells"
                        onClicked: grid.horizontalLines += 1
                        Layout.alignment: Qt.AlignHCenter

                    }
                    RowLayout {
                        Layout.alignment: Qt.AlignHCenter
                        Button  {
                            text: "- X Cells"
                            onClicked: grid.verticalLines -= 1
                        }
                        Button {
                            text: "+ X Cells"
                            onClicked: grid.verticalLines += 1
                        }
                    }
                    Button  {
                        text: "- Y Cells"
                        onClicked: grid.horizontalLines -= 1
                        Layout.alignment: Qt.AlignHCenter
                    }

                    Label {
                        text: "Line width (if supported)"
                    }
                    Slider {
                        Layout.fillWidth: true
                        id: sliderLineWidth
                        from: 1.0
                        to: 10.0
                        stepSize: 0.5
                        value: 1.0
                    }
                }
            }
            Pane {
                id: triangleSettings
                visible: false
                ColumnLayout {
                    CheckBox {
                        id: cbNorm
                        text: "provide normals in geometry"
                        checked: false
                    }
                    RowLayout {
                        enabled: cbNorm.checked
                        Label {
                            Layout.fillWidth: true
                            text: "Normal adjust: "
                        }
                        Slider {
                            id: sliderNorm

                            from: 0.0
                            to: 1.0
                            stepSize: 0.01
                            value: 0.0
                        }
                    }
                    CheckBox {
                        id: cbTexture
                        text: "enable base color map"
                        checked: false
                    }
                    CheckBox {
                        id: cbUV
                        text: "provide UV in geometry"
                        checked: false
                    }
                    RowLayout {
                        enabled: cbUV.checked
                        Label {
                            Layout.fillWidth: true
                            text: "UV adjust:"
                        }
                        Slider {
                            id: sliderUV
                            from: 0.0
                            to: 1.0
                            stepSize: 0.01
                            value: 0.0
                        }
                    }
                }

            }
            Pane {
                id: pointSettings
                visible: false
                RowLayout {
                    ColumnLayout {
                        RowLayout {
                            Label {
                                text: "Point size (if supported)"
                            }
                            Slider {
                                id: sliderPointSize
                                from: 1.0
                                to: 16.0
                                stepSize: 1.0
                                value: 1.0
                            }
                        }
                    }
                }
            }
            Pane {
                id: torusSettings
                visible: false
                ColumnLayout {
                    Label {
                        text: "Radius: (" + radiusSlider.value + ")"
                    }
                    Slider {
                        id: radiusSlider
                        from: 1.0
                        to: 1000.0
                        stepSize: 1.0
                        value: 200
                    }
                    Label {
                        text: "Tube Radius: (" + tubeRadiusSlider.value + ")"
                    }
                    Slider {
                        id: tubeRadiusSlider
                        from: 1.0
                        to: 500.0
                        stepSize: 1.0
                        value: 50
                    }
                    Label {
                        text: "Rings: (" + ringsSlider.value + ")"
                    }
                    Slider {
                        id: ringsSlider
                        from: 3
                        to: 35
                        stepSize: 1.0
                        value: 20
                    }
                    Label {
                        text: "Segments: (" + segmentsSlider.value + ")"
                    }
                    Slider {
                        id: segmentsSlider
                        from: 3
                        to: 35
                        stepSize: 1.0
                        value: 20
                    }
                    CheckBox {
                        id: wireFrameCheckbox
                        text: "Wireframe Mode"
                        checked: false
                        onCheckedChanged: {
                            env.debugSettings.wireframeEnabled = checked
                            torusMaterial.cullMode = checked ? Material.NoCulling : Material.BackFaceCulling


                        }
                    }
                }

            }
        }
        states: [
            State {
                name: "gridMode"
                when: radioGridGeom.checked
                PropertyChanges {
                    gridModel.visible: true
                    gridSettings.visible: true
                    env.debugSettings.wireframeEnabled: false
                    originNode.position: Qt.vector3d(0, 0, 0)
                    originNode.rotation: Qt.quaternion(1, 0, 0, 0)
                    cameraNode.z: 600

                }
            },
            State {
                name: "triangleMode"
                when: radioCustGeom.checked
                PropertyChanges {
                    triangleModel.visible: true
                    triangleSettings.visible: true
                    env.debugSettings.wireframeEnabled: false
                    originNode.position: Qt.vector3d(0, 0, 0)
                    originNode.rotation: Qt.quaternion(1, 0, 0, 0)
                    cameraNode.z: 600
                }
            },
            State {
                name: "pointMode"
                when: radioPointGeom.checked
                PropertyChanges {
                    pointModel.visible: true
                    pointSettings.visible: true
                    env.debugSettings.wireframeEnabled: false
                    originNode.position: Qt.vector3d(0, 0, 0)
                    originNode.rotation: Qt.quaternion(1, 0, 0, 0)
                    cameraNode.z: 600
                }
            },
            State {
                name: "qmlMode"
                when: radioQMLGeom.checked
                PropertyChanges {
                    torusModel.visible: true
                    torusSettings.visible: true
                    directionalLight.eulerRotation: Qt.vector3d(-40, 0, 0)
                    directionalLight.color: "white"
                    pointLight.color: "white"
                    pointLight.position: Qt.vector3d(0, 0, 0)
                    originNode.position: Qt.vector3d(0, 0, 0)
                    originNode.eulerRotation: Qt.vector3d(-40, 0, 0)
                    cameraNode.z: 600
                }
            }
        ]
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick3D.Helpers

ProceduralMesh {
    property int rings: 50
    property int segments: 50
    property real radius: 100.0
    property real tubeRadius: 10.0
    property var meshArrays: generateTorus(rings, segments, radius, tubeRadius)
    positions: meshArrays.verts
    normals: meshArrays.normals
    uv0s: meshArrays.uvs
    indexes: meshArrays.indices

    function generateTorus(rings: int, segments: int, radius: real, tubeRadius: real) : var {
        let verts = []
        let normals = []
        let uvs = []
        let indices = []

        for (let i = 0; i <= rings; ++i) {
            for (let j = 0; j <= segments; ++j) {
                const u = i / rings * Math.PI * 2;
                const v = j / segments * Math.PI * 2;

                const centerX = radius * Math.cos(u);
                const centerZ = radius * Math.sin(u);

                const posX = centerX + tubeRadius * Math.cos(v) * Math.cos(u);
                const posY = tubeRadius * Math.sin(v);
                const posZ = centerZ + tubeRadius * Math.cos(v) * Math.sin(u);

                verts.push(Qt.vector3d(posX, posY, posZ));

                const normal = Qt.vector3d(posX - centerX, posY, posZ - centerZ).normalized();
                normals.push(normal);

                uvs.push(Qt.vector2d(i / rings, j / segments));
            }
        }

        for (let i = 0; i < rings; ++i) {
            for (let j = 0; j < segments; ++j) {
                const a = (segments + 1) * i + j;
                const b = (segments + 1) * (i + 1) + j;
                const c = (segments + 1) * (i + 1) + j + 1;
                const d = (segments + 1) * i + j + 1;

                // Generate two triangles for each quad in the mesh
                // Adjust order to be counter-clockwise
                indices.push(a, d, b);
                indices.push(b, d, c);
            }
        }
        return { verts: verts, normals: normals, uvs: uvs, indices: indices }
    }
}
module CustomGeometryExample
Main 1.0 Main.qml
TorusMesh 1.0 TorusMesh.qml