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.

# 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