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.
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>
© 2022 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.