Revision control

Copy as Markdown

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "proceduralmesh_p.h"
#include <QtQuick3D/private/qquick3dobject_p.h>
#include <QtQuick/QQuickWindow>
#include <rhi/qrhi.h>
QT_BEGIN_NAMESPACE
/*!
\qmltype ProceduralMesh
\inqmlmodule QtQuick3D.Helpers
\inherits Geometry
\brief Allows creation of Geometry from QML.
\since 6.6
ProceduralMesh is a helper type that allows creation of Geometry instances
from QML. The Geometry component is Abstract, and is usually created
from C++.
\qml
component TorusMesh : ProceduralMesh {
property real rings: 50
property real 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: real, segments: real, radius: real, tubeRadius: real) {
let verts = []
let normals = []
let uvs = []
let indices = []
for (let i = 0; i <= rings; ++i) {
for (let j = 0; j <= segments; ++j) {
let u = i / rings * Math.PI * 2;
let v = j / segments * Math.PI * 2;
let centerX = radius * Math.cos(u);
let centerZ = radius * Math.sin(u);
let posX = centerX + tubeRadius * Math.cos(v) * Math.cos(u);
let posY = tubeRadius * Math.sin(v);
let posZ = centerZ + tubeRadius * Math.cos(v) * Math.sin(u);
verts.push(Qt.vector3d(posX, posY, posZ));
let 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) {
let a = (segments + 1) * i + j;
let b = (segments + 1) * (i + 1) + j;
let c = (segments + 1) * (i + 1) + j + 1;
let 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 }
}
}
\endqml
The above code defines a component TorusMesh that can be used as Geometry for use
with a Model component. When the ring, segments, radius or tubeRadius properties
are modified the geometry will be updated.
The ProceduralMesh component is not as flexible nor as performant as creating
Geometry in C++, but makes up for it in convenience and simplicity. The
properties are fixed attribute lists that when filled will automatically
generate the necessary buffers.
*/
/*!
\qmlproperty List<QVector3D> ProceduralMesh::positions
The positions attribute list. If this list remains empty nothing no geometry
will be generated.
*/
/*!
\qmlproperty List<QVector3D> ProceduralMesh::normals
Holds the normals attribute list.
*/
/*!
\qmlproperty List<QVector3D> ProceduralMesh::tangents
Holds the tangents attribute list.
*/
/*!
\qmlproperty List<QVector3D> ProceduralMesh::binormals
Holds the binormals attribute list.
*/
/*!
\qmlproperty List<QVector2D> ProceduralMesh::uv0s
This property defines a list of uv coordinates for the first uv channel (uv0)
*/
/*!
\qmlproperty List<QVector2D> ProceduralMesh::uv1s
This property defines a list of uv coordinates for the second uv channel (uv1)
*/
/*!
\qmlproperty List<QVector4D> ProceduralMesh::colors
This property defines a list of vertex color values.
*/
/*!
\qmlproperty List<QVector4D> ProceduralMesh::joints
This property defines a list of joint indices for skinning.
*/
/*!
\qmlproperty List<QVector4D> ProceduralMesh::weights
This property defines a list of joint weights for skinning.
*/
/*!
\qmlproperty List<int> ProceduralMesh::indexes
This property defines a list of indexes into the attribute lists. If this list remains empty
the vertex buffer values will be used directly.
*/
/*!
\qmlproperty enumeration ProceduralMesh::primitiveMode
This property defines the primitive mode to use when rendering the geometry.
\value ProceduralMesh.Points The points primitive mode is used.
\value ProceduralMesh.LineStrip The line strip primitive mode is used.
\value ProceduralMesh.Lines The lines primitive mode is used.
\value ProceduralMesh.TriangleStrip The triangles strip primitive mode is
used.
\value ProceduralMesh.TriangleFan The triangle fan primitive mode is used.
\value ProceduralMesh.Triangles The triangles primitive mode is used.
\default ProceduralMesh.Triangles
\note Not all modes are supported on all rendering backends.
*/
/*!
\qmlproperty List<ProceduralMeshSubset> ProceduralMesh::subsets
This property defines a list of subsets to split the geometry data into.
Each subset can have it's own material. The order of this array
corresponds to the materials list of Model when using this geometry.
This property is optional and when empty results in a single subset.
\note Any subset that specifies values outside of the range of available
vertex/index values will lead to that subset being ignored.
*/
/*!
\qmltype ProceduralMeshSubset
\inqmlmodule QtQuick3D.Helpers
\inherits QtObject
\brief Defines a subset of a ProceduralMesh.
\since 6.6
This type defines a subset of a ProceduralMesh. Each subset can have it's own
material and can be used to split the geometry into multiple draw calls.
\sa ProceduralMesh::subsets
*/
/*!
\qmlproperty int ProceduralMeshSubset::offset
This property defines the starting index for this subset. \default 0
*/
/*!
\qmlproperty int ProceduralMeshSubset::count
This property defines the number of indices to use for this subset. This property must be set for the subset to have content.
\default 0
*/
/*!
\qmlproperty Material ProceduralMeshSubset::name
This property defines a name of the subset. This property is optional, and is only used to
tag the subset for debugging purposes.
*/
ProceduralMesh::ProceduralMesh()
{
}
QList<QVector3D> ProceduralMesh::positions() const
{
return m_positions;
}
void ProceduralMesh::setPositions(const QList<QVector3D> &newPositions)
{
if (m_positions == newPositions)
return;
m_positions = newPositions;
Q_EMIT positionsChanged();
requestUpdate();
}
ProceduralMesh::PrimitiveMode ProceduralMesh::primitiveMode() const
{
return m_primitiveMode;
}
void ProceduralMesh::setPrimitiveMode(PrimitiveMode newPrimitiveMode)
{
if (m_primitiveMode == newPrimitiveMode)
return;
// Do some sanity checking
if (newPrimitiveMode < Points || newPrimitiveMode > Triangles) {
qWarning() << "Invalid primitive mode specified";
return;
}
if (newPrimitiveMode == PrimitiveMode::TriangleFan) {
if (!supportsTriangleFanPrimitive()) {
qWarning() << "TriangleFan is not supported by the current backend";
return;
}
}
m_primitiveMode = newPrimitiveMode;
Q_EMIT primitiveModeChanged();
requestUpdate();
}
void ProceduralMesh::requestUpdate()
{
if (!m_updateRequested) {
QMetaObject::invokeMethod(this, "updateGeometry", Qt::QueuedConnection);
m_updateRequested = true;
}
}
void ProceduralMesh::updateGeometry()
{
m_updateRequested = false;
// reset the geometry
clear();
setPrimitiveType(PrimitiveType(m_primitiveMode));
// Figure out which attributes are being used
const auto expectedLength = m_positions.size();
bool hasPositions = !m_positions.isEmpty();
if (!hasPositions) {
setStride(0);
update();
return; // If there are no positions, there is no point :-)
}
bool hasNormals = m_normals.size() >= expectedLength;
bool hasTangents = m_tangents.size() >= expectedLength;
bool hasBinormals = m_binormals.size() >= expectedLength;
bool hasUV0s = m_uv0s.size() >= expectedLength;
bool hasUV1s = m_uv1s.size() >= expectedLength;
bool hasColors = m_colors.size() >= expectedLength;
bool hasJoints = m_joints.size() >= expectedLength;
bool hasWeights = m_weights.size() >= expectedLength;
bool hasIndexes = !m_indexes.isEmpty();
int offset = 0;
if (hasPositions) {
addAttribute(Attribute::Semantic::PositionSemantic, offset, Attribute::ComponentType::F32Type);
offset += 3 * sizeof(float);
}
if (hasNormals) {
addAttribute(Attribute::Semantic::NormalSemantic, offset, Attribute::ComponentType::F32Type);
offset += 3 * sizeof(float);
}
if (hasTangents) {
addAttribute(Attribute::Semantic::TangentSemantic, offset, Attribute::ComponentType::F32Type);
offset += 3 * sizeof(float);
}
if (hasBinormals) {
addAttribute(Attribute::Semantic::BinormalSemantic, offset, Attribute::ComponentType::F32Type);
offset += 3 * sizeof(float);
}
if (hasUV0s) {
addAttribute(Attribute::Semantic::TexCoord0Semantic, offset, Attribute::ComponentType::F32Type);
offset += 2 * sizeof(float);
}
if (hasUV1s) {
addAttribute(Attribute::Semantic::TexCoord1Semantic, offset, Attribute::ComponentType::F32Type);
offset += 2 * sizeof(float);
}
if (hasColors) {
addAttribute(Attribute::Semantic::ColorSemantic, offset, Attribute::ComponentType::F32Type);
offset += 4 * sizeof(float);
}
if (hasJoints) {
addAttribute(Attribute::Semantic::JointSemantic, offset, Attribute::ComponentType::F32Type);
offset += 4 * sizeof(float);
}
if (hasWeights) {
addAttribute(Attribute::Semantic::WeightSemantic, offset, Attribute::ComponentType::F32Type);
offset += 4 * sizeof(float);
}
if (hasIndexes)
addAttribute(Attribute::Semantic::IndexSemantic, 0, Attribute::ComponentType::U32Type);
// Set up the vertex buffer
const int stride = offset;
const qsizetype bufferSize = expectedLength * stride;
setStride(stride);
QVector<float> vertexBufferData;
vertexBufferData.reserve(bufferSize / sizeof(float));
QVector3D minBounds;
QVector3D maxBounds;
for (qsizetype i = 0; i < expectedLength; ++i) {
// start writing float values to vertexBuffer
if (hasPositions) {
const auto &position = m_positions[i];
vertexBufferData.append(position.x());
vertexBufferData.append(position.y());
vertexBufferData.append(position.z());
minBounds.setX(qMin(minBounds.x(), position.x()));
maxBounds.setX(qMax(maxBounds.x(), position.x()));
minBounds.setY(qMin(minBounds.y(), position.y()));
maxBounds.setY(qMax(maxBounds.y(), position.y()));
minBounds.setZ(qMin(minBounds.z(), position.z()));
maxBounds.setZ(qMax(maxBounds.z(), position.z()));
}
if (hasNormals) {
const auto &normal = m_normals[i];
vertexBufferData.append(normal.x());
vertexBufferData.append(normal.y());
vertexBufferData.append(normal.z());
}
if (hasBinormals) {
const auto &binormal = m_binormals[i];
vertexBufferData.append(binormal.x());
vertexBufferData.append(binormal.y());
vertexBufferData.append(binormal.z());
}
if (hasTangents) {
const auto &tangent = m_tangents[i];
vertexBufferData.append(tangent.x());
vertexBufferData.append(tangent.y());
vertexBufferData.append(tangent.z());
}
if (hasUV0s) {
const auto &uv0 = m_uv0s[i];
vertexBufferData.append(uv0.x());
vertexBufferData.append(uv0.y());
}
if (hasUV1s) {
const auto &uv1 = m_uv1s[i];
vertexBufferData.append(uv1.x());
vertexBufferData.append(uv1.y());
}
if (hasColors) {
const auto &color = m_colors[i];
vertexBufferData.append(color.x());
vertexBufferData.append(color.y());
vertexBufferData.append(color.z());
vertexBufferData.append(color.w());
}
if (hasJoints) {
const auto &joint = m_joints[i];
vertexBufferData.append(joint.x());
vertexBufferData.append(joint.y());
vertexBufferData.append(joint.z());
vertexBufferData.append(joint.w());
}
if (hasWeights) {
const auto &weight = m_weights[i];
vertexBufferData.append(weight.x());
vertexBufferData.append(weight.y());
vertexBufferData.append(weight.z());
vertexBufferData.append(weight.w());
}
}
setBounds(minBounds, maxBounds);
QByteArray vertexBuffer(reinterpret_cast<char *>(vertexBufferData.data()), bufferSize);
setVertexData(vertexBuffer);
// Index Buffer
if (hasIndexes) {
const qsizetype indexLength = m_indexes.size();
QByteArray indexBuffer;
indexBuffer.reserve(indexLength * sizeof(unsigned int));
for (qsizetype i = 0; i < indexLength; ++i) {
const auto &index = m_indexes[i];
indexBuffer.append(reinterpret_cast<const char *>(&index), sizeof(unsigned int));
}
setIndexData(indexBuffer);
}
// Subsets
// Subsets are optional so if none are specified the whole mesh is a single submesh
if (!m_subsets.isEmpty()) {
for (const auto &subset : std::as_const(m_subsets)) {
QVector3D subsetMinBounds;
QVector3D subsetMaxBounds;
// Range checking is necessary because the user could have specified subset values
// that are out of range of the vertex/index buffer
bool outOfRange = false;
for (qsizetype i = subset->offset(); i < subset->offset() + subset->count(); ++i) {
if (hasPositions) {
qsizetype index = i;
if (hasIndexes) {
if (i < m_indexes.size()) {
index = m_indexes[i];
} else {
outOfRange = true;
break;
}
}
if (index < m_positions.size()) {
const auto &position = m_positions[index];
subsetMinBounds.setX(qMin(subsetMinBounds.x(), position.x()));
subsetMaxBounds.setX(qMax(subsetMaxBounds.x(), position.x()));
subsetMinBounds.setY(qMin(subsetMinBounds.y(), position.y()));
subsetMaxBounds.setY(qMax(subsetMaxBounds.y(), position.y()));
subsetMinBounds.setZ(qMin(subsetMinBounds.z(), position.z()));
subsetMaxBounds.setZ(qMax(subsetMaxBounds.z(), position.z()));
} else {
outOfRange = true;
break;
}
}
}
if (!outOfRange)
addSubset(subset->offset(), subset->count(), subsetMinBounds, subsetMaxBounds, subset->name());
else
qWarning("Skipping invalid subset: Out of Range");
}
}
update();
emit geometryChanged();
}
void ProceduralMesh::subsetDestroyed(QObject *subset)
{
if (m_subsets.removeAll(subset))
requestUpdate();
}
bool ProceduralMesh::supportsTriangleFanPrimitive() const
{
static bool supportQueried = false;
static bool triangleFanSupported = false;
if (!supportQueried) {
const auto &manager = QQuick3DObjectPrivate::get(this)->sceneManager;
if (manager) {
auto window = manager->window();
if (window) {
auto rhi = window->rhi();
if (rhi) {
triangleFanSupported = rhi->isFeatureSupported(QRhi::TriangleFanTopology);
supportQueried = true;
}
}
}
}
return triangleFanSupported;
}
void ProceduralMesh::qmlAppendProceduralMeshSubset(QQmlListProperty<ProceduralMeshSubset> *list, ProceduralMeshSubset *subset)
{
if (subset == nullptr)
return;
ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object);
self->m_subsets.push_back(subset);
connect(subset, &ProceduralMeshSubset::isDirty, self, &ProceduralMesh::requestUpdate);
connect(subset, &QObject::destroyed, self, &ProceduralMesh::subsetDestroyed);
self->requestUpdate();
}
ProceduralMeshSubset *ProceduralMesh::qmlProceduralMeshSubsetAt(QQmlListProperty<ProceduralMeshSubset> *list, qsizetype index)
{
ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object);
return self->m_subsets.at(index);
}
qsizetype ProceduralMesh::qmlProceduralMeshSubsetCount(QQmlListProperty<ProceduralMeshSubset> *list)
{
ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object);
return self->m_subsets.count();
}
void ProceduralMesh::qmlClearProceduralMeshSubset(QQmlListProperty<ProceduralMeshSubset> *list)
{
ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object);
self->m_subsets.clear();
self->requestUpdate();
}
QList<unsigned int> ProceduralMesh::indexes() const
{
return m_indexes;
}
void ProceduralMesh::setIndexes(const QList<unsigned int> &newIndexes)
{
if (m_indexes == newIndexes)
return;
m_indexes = newIndexes;
Q_EMIT indexesChanged();
requestUpdate();
}
QList<QVector3D> ProceduralMesh::normals() const
{
return m_normals;
}
void ProceduralMesh::setNormals(const QList<QVector3D> &newNormals)
{
if (m_normals == newNormals)
return;
m_normals = newNormals;
Q_EMIT normalsChanged();
requestUpdate();
}
QList<QVector3D> ProceduralMesh::tangents() const
{
return m_tangents;
}
void ProceduralMesh::setTangents(const QList<QVector3D> &newTangents)
{
if (m_tangents == newTangents)
return;
m_tangents = newTangents;
Q_EMIT tangentsChanged();
requestUpdate();
}
QList<QVector3D> ProceduralMesh::binormals() const
{
return m_binormals;
}
void ProceduralMesh::setBinormals(const QList<QVector3D> &newBinormals)
{
if (m_binormals == newBinormals)
return;
m_binormals = newBinormals;
Q_EMIT binormalsChanged();
requestUpdate();
}
QList<QVector2D> ProceduralMesh::uv0s() const
{
return m_uv0s;
}
void ProceduralMesh::setUv0s(const QList<QVector2D> &newUv0s)
{
if (m_uv0s == newUv0s)
return;
m_uv0s = newUv0s;
Q_EMIT uv0sChanged();
requestUpdate();
}
QList<QVector2D> ProceduralMesh::uv1s() const
{
return m_uv1s;
}
void ProceduralMesh::setUv1s(const QList<QVector2D> &newUv1s)
{
if (m_uv1s == newUv1s)
return;
m_uv1s = newUv1s;
Q_EMIT uv1sChanged();
requestUpdate();
}
QList<QVector4D> ProceduralMesh::colors() const
{
return m_colors;
}
void ProceduralMesh::setColors(const QList<QVector4D> &newColors)
{
if (m_colors == newColors)
return;
m_colors = newColors;
Q_EMIT colorsChanged();
requestUpdate();
}
QList<QVector4D> ProceduralMesh::joints() const
{
return m_joints;
}
void ProceduralMesh::setJoints(const QList<QVector4D> &newJoints)
{
if (m_joints == newJoints)
return;
m_joints = newJoints;
Q_EMIT jointsChanged();
requestUpdate();
}
QList<QVector4D> ProceduralMesh::weights() const
{
return m_weights;
}
void ProceduralMesh::setWeights(const QList<QVector4D> &newWeights)
{
if (m_weights == newWeights)
return;
m_weights = newWeights;
Q_EMIT weightsChanged();
requestUpdate();
}
QQmlListProperty<ProceduralMeshSubset> ProceduralMesh::subsets()
{
return QQmlListProperty<ProceduralMeshSubset>(this,
nullptr,
ProceduralMesh::qmlAppendProceduralMeshSubset,
ProceduralMesh::qmlProceduralMeshSubsetCount,
ProceduralMesh::qmlProceduralMeshSubsetAt,
ProceduralMesh::qmlClearProceduralMeshSubset);
}
int ProceduralMeshSubset::offset() const
{
return m_offset;
}
void ProceduralMeshSubset::setOffset(int newOffset)
{
if (m_offset == newOffset)
return;
m_offset = newOffset;
Q_EMIT offsetChanged();
Q_EMIT isDirty();
}
int ProceduralMeshSubset::count() const
{
return m_count;
}
void ProceduralMeshSubset::setCount(int newCount)
{
if (m_count == newCount)
return;
m_count = newCount;
Q_EMIT countChanged();
Q_EMIT isDirty();
}
QString ProceduralMeshSubset::name() const
{
return m_name;
}
void ProceduralMeshSubset::setName(const QString &newName)
{
if (m_name == newName)
return;
m_name = newName;
Q_EMIT nameChanged();
Q_EMIT isDirty();
}
QT_END_NAMESPACE