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
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>