Revision control
++ // Copyright (C) 2018 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include "qwavefrontmesh_p.h"
++
++ #include <QtCore/qfile.h>
++ #include <QtCore/qtextstream.h>
++ #include <QtCore/private/qobject_p.h>
++
++ #include <QtGui/qvector2d.h>
++ #include <QtGui/qvector3d.h>
++
++ #include <QtQml/qqmlfile.h>
++ #include <QtQml/qqmlcontext.h>
++
++ #include <QtQuick/qsggeometry.h>
++
++ QT_BEGIN_NAMESPACE
++
++ class QWavefrontMeshPrivate : public QObjectPrivate
++ {
++ public:
++ QWavefrontMeshPrivate()
++ : lastError(QWavefrontMesh::NoError)
++ {}
++
++ Q_DECLARE_PUBLIC(QWavefrontMesh)
++
++ static QWavefrontMeshPrivate *get(QWavefrontMesh *mesh)
++ {
++ return mesh->d_func();
++ }
++
++ static const QWavefrontMeshPrivate *get(const QWavefrontMesh *mesh)
++ {
++ return mesh->d_func();
++ }
++
++ QList<std::pair<ushort, ushort> > indexes;
++ QList<QVector3D> vertexes;
++ QList<QVector2D> textureCoordinates;
++
++ QUrl source;
++ QWavefrontMesh::Error lastError;
++
++ QVector3D planeV;
++ QVector3D planeW;
++ };
++
++ /*!
++ \qmlmodule Qt.labs.wavefrontmesh 1.\QtMinorVersion
++ \title Qt Labs WavefrontMesh QML Types
++ \ingroup qmlmodules
++ \brief The WavefrontMesh provides a mesh based on a Wavefront .obj file.
++
++ To use this module, import the module with the following line:
++
++ \qml
++ import Qt.labs.wavefrontmesh
++ \endqml
++ */
++
++ /*!
++ \qmltype WavefrontMesh
++ \inqmlmodule Qt.labs.wavefrontmesh
++ //! \nativetype QWavefrontMesh
++ \ingroup qtquick-effects
++ \brief The WavefrontMesh provides a mesh based on a Wavefront .obj file.
++ \since 5.12
++
++ WavefrontMesh reads the geometry from a Wavefront .obj file and generates
++ a two-dimensional \l{QSGGeometry}{geometry} from this. If the .obj file
++ contains a three-dimensional shape, it will be orthographically projected,
++ onto a plane. If defined, this is given by \l projectionPlaneV
++ and \l projectionPlaneW. Otherwise, the first face encountered in the data
++ will be used to determine the projection plane.
++
++ If the file contains texture coordinates, these will also be used. Otherwise,
++ the vertexes of the object will be normalized and used.
++
++ The mesh can be used in a ShaderEffect to define the shaded geometry. The
++ geometry will be normalized before use, so the position and scale of the
++ input objects have no impact on the result.
++
++ \note Some Wavefront exporters will change the source scene's coordinate system
++ before exporting it. This can cause unexpected results when Qt applies the
++ projection. If the visual results are not as you expect, try checking the export
++ parameters and the documentation of the editor tool to see if this is the case.
++
++ For instance, the following example takes an .obj file containing a standard torus
++ and visualizes the automatically generated texture coordinates.
++
++ \table
++ \row
++ \li \image qtlabs-wavefrontmesh.png
++ {Torus shape with gradient texture coordinates}
++ \li \qml
++ import QtQuick 2.\1
++ import Qt.labs.wavefrontmesh 1.\1
++
++ ShaderEffect {
++ width: 200
++ height: 200
++ mesh: WavefrontMesh {
++ source: "torus.obj"
++ projectionPlaneV: Qt.vector3d(0, 1, 0)
++ projectionPlaneW: Qt.vector3d(1, 0, 0)
++ }
++ vertexShader: "
++ uniform highp mat4 qt_Matrix;
++ attribute highp vec4 qt_Vertex;
++ attribute highp vec2 qt_MultiTexCoord0;
++ varying highp vec2 coord;
++ void main() {
++ coord = qt_MultiTexCoord0;
++ gl_Position = qt_Matrix * qt_Vertex;
++ }"
++ fragmentShader: "
++ varying highp vec2 coord;
++ uniform lowp float qt_Opacity;
++ void main() {
++ gl_FragColor = vec4(coord.x, coord.y, 0.0, 1.0);
++ }"
++
++ }
++ \endqml
++ \endtable
++
++ \note Since the input is a 3D torus, we need to define the projection plane. This would not be
++ necessary when using a 2D shape as input. We use the XY plane in this case, because of the
++ orientation of the input.
++ */
++
++ QWavefrontMesh::QWavefrontMesh(QObject *parent)
++ : QQuickShaderEffectMesh(*(new QWavefrontMeshPrivate), parent)
++ {
++ connect(this, &QWavefrontMesh::sourceChanged, this, &QWavefrontMesh::readData);
++ connect(this, &QWavefrontMesh::projectionPlaneVChanged, this, &QQuickShaderEffectMesh::geometryChanged);
++ connect(this, &QWavefrontMesh::projectionPlaneWChanged, this, &QQuickShaderEffectMesh::geometryChanged);
++ }
++
++ QWavefrontMesh::~QWavefrontMesh()
++ {
++ }
++
++ /*!
++ \qmlproperty enumeration WavefrontMesh::lastError
++
++ This property holds the last error, if any, that occurred when parsing the
++ source or building the mesh.
++
++ \list
++ \li WavefrontMesh.NoError No error has occurred.
++ \li WavefrontMesh.InvalidSourceError The source was not recognized as a valid .obj file.
++ \li WavefrontMesh.UnsupportedFaceShapeError The faces in the source is of an unsupported type.
++ WavefrontMesh only supports triangles and convex quads.
++ \li WavefrontMesh.UnsupportedIndexSizeError The source shape is too large. Only 16 bit indexes are supported.
++ \li WavefrontMesh.FileNotFoundError The source file was not found.
++ \li WavefrontMesh.MissingPositionAttributeError The 'qt_Vertex' attribute is missing from the shaders.
++ \li WavefrontMesh.MissingTextureCoordinateAttributeError The texture coordinate attribute in the shaders is wrongly named. Use 'qt_MultiTexCoord0'.
++ \li WavefrontMesh.MissingPositionAndTextureCoordinateAttributesError Both the 'qt_Vertex' and 'qt_MultiTexCoord0' attributes are missing from the shaders.
++ \li WavefrontMesh.TooManyAttributesError The shaders expect too many attributes (maximum is two: Position, 'qt_Vertex', and texture coordinate, 'qt_MultiTexCoord0').
++ \li WavefrontMesh.InvalidPlaneDefinitionError The V and W vectors in the plane cannot be null, nor parallel to each other.
++ \endlist
++ */
++
++ QWavefrontMesh::Error QWavefrontMesh::lastError() const
++ {
++ Q_D(const QWavefrontMesh);
++ return d->lastError;
++ }
++
++ void QWavefrontMesh::setLastError(Error lastError)
++ {
++ Q_D(QWavefrontMesh);
++ if (d->lastError == lastError)
++ return;
++
++ d->lastError = lastError;
++ emit lastErrorChanged();
++ }
++
++ /*!
++ \qmlproperty url WavefrontMesh::source
++
++ This property holds the URL of the source. This must be either a local file or in qrc. The source will
++ be read as a Wavefront .obj file and the geometry will be updated.
++ */
++ QUrl QWavefrontMesh::source() const
++ {
++ Q_D(const QWavefrontMesh);
++ return d->source;
++ }
++
++ void QWavefrontMesh::setSource(const QUrl &source)
++ {
++ Q_D(QWavefrontMesh);
++ if (d->source == source)
++ return;
++
++ d->source = source;
++ emit sourceChanged();
++ }
++
++ void QWavefrontMesh::readData()
++ {
++ Q_D(QWavefrontMesh);
++ d->vertexes.clear();
++ d->textureCoordinates.clear();
++ d->indexes.clear();
++
++ QString localFile = QQmlFile::urlToLocalFileOrQrc(d->source);
++ if (!localFile.isEmpty()) {
++ QFile file(localFile);
++ if (file.open(QIODevice::ReadOnly)) {
++ QTextStream stream(&file);
++
++ QString buffer;
++ buffer.reserve(256);
++
++ static QChar space(QLatin1Char(' '));
++ static QChar slash(QLatin1Char('/'));
++
++ while (!stream.atEnd()) {
++ stream.readLineInto(&buffer);
++ auto tokens = QStringView{buffer}.split(space, Qt::SkipEmptyParts);
++ if (tokens.size() < 2)
++ continue;
++
++ QByteArray command = tokens.at(0).toLatin1();
++
++ if (command == "vt") {
++ bool ok;
++ float u = tokens.at(1).toFloat(&ok);
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ float v = tokens.size() > 2 ? tokens.at(2).toFloat(&ok) : 0.0;
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ d->textureCoordinates.append(QVector2D(u, v));
++ } else if (command == "v") {
++ // Format: v <x> <y> <z> [w]
++ if (tokens.size() < 4 || tokens.size() > 5) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ bool ok;
++
++ float x = tokens.at(1).toFloat(&ok);
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ float y = tokens.at(2).toFloat(&ok);
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ float z = tokens.at(3).toFloat(&ok);
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ d->vertexes.append(QVector3D(x, y, z));
++ } else if (command == "f") {
++ // The scenegraph only supports triangles, so we
++ // support triangles and quads (which we split up)
++ int p1, p2, p3;
++ int t1 = 0;
++ int t2 = 0;
++ int t3 = 0;
++ if (tokens.size() >= 4 && tokens.size() <= 5) {
++ {
++ bool ok;
++ auto faceTokens = tokens.at(1).split(slash, Qt::SkipEmptyParts);
++ Q_ASSERT(!faceTokens.isEmpty());
++
++ p1 = faceTokens.at(0).toInt(&ok) - 1;
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ if (faceTokens.size() > 1) {
++ t1 = faceTokens.at(1).toInt(&ok) - 1;
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++ }
++ }
++
++ {
++ bool ok;
++ auto faceTokens = tokens.at(2).split(slash, Qt::SkipEmptyParts);
++ Q_ASSERT(!faceTokens.isEmpty());
++
++ p2 = faceTokens.at(0).toInt(&ok) - 1;
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ if (faceTokens.size() > 1) {
++ t2 = faceTokens.at(1).toInt(&ok) - 1;
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++ }
++ }
++
++ {
++ bool ok;
++ auto faceTokens = tokens.at(3).split(slash, Qt::SkipEmptyParts);
++ Q_ASSERT(!faceTokens.isEmpty());
++
++ p3 = faceTokens.at(0).toInt(&ok) - 1;
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ if (faceTokens.size() > 1) {
++ t3 = faceTokens.at(1).toInt(&ok) - 1;
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++ }
++ }
++
++ if (Q_UNLIKELY(p1 < 0 || p1 > UINT16_MAX
++ || p2 < 0 || p2 > UINT16_MAX
++ || p3 < 0 || p3 > UINT16_MAX
++ || t1 < 0 || t1 > UINT16_MAX
++ || t2 < 0 || t2 > UINT16_MAX
++ || t3 < 0 || t3 > UINT16_MAX)) {
++ setLastError(UnsupportedIndexSizeError);
++ return;
++ }
++
++ d->indexes.append(std::make_pair(ushort(p1), ushort(t1)));
++ d->indexes.append(std::make_pair(ushort(p2), ushort(t2)));
++ d->indexes.append(std::make_pair(ushort(p3), ushort(t3)));
++ } else {
++ setLastError(UnsupportedFaceShapeError);
++ return;
++ }
++
++ if (tokens.size() == 5) {
++ bool ok;
++ auto faceTokens = tokens.at(4).split(slash, Qt::SkipEmptyParts);
++ Q_ASSERT(!faceTokens.isEmpty());
++
++ int p4 = faceTokens.at(0).toInt(&ok) - 1;
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++
++ int t4 = 0;
++ if (faceTokens.size() > 1) {
++ t4 = faceTokens.at(1).toInt(&ok) - 1;
++ if (!ok) {
++ setLastError(InvalidSourceError);
++ return;
++ }
++ }
++
++ if (Q_UNLIKELY(p4 < 0 || p4 > UINT16_MAX || t4 < 0 || t4 > UINT16_MAX)) {
++ setLastError(UnsupportedIndexSizeError);
++ return;
++ }
++
++ // ### Assumes convex quad, correct algorithm is to find the concave corner,
++ // and if there is one, do the split on the line between this and the corner it is
++ // not connected to. Also assumes order of vertices is counter clockwise.
++ d->indexes.append(std::make_pair(ushort(p3), ushort(t3)));
++ d->indexes.append(std::make_pair(ushort(p4), ushort(t4)));
++ d->indexes.append(std::make_pair(ushort(p1), ushort(t1)));
++ }
++ }
++ }
++ } else {
++ setLastError(FileNotFoundError);
++ }
++ } else {
++ setLastError(InvalidSourceError);
++ }
++
++ emit geometryChanged();
++ }
++
++ QString QWavefrontMesh::log() const
++ {
++ Q_D(const QWavefrontMesh);
++ switch (d->lastError) {
++ case NoError:
++ return QStringLiteral("No error");
++ case InvalidSourceError:
++ return QStringLiteral("Error: Invalid source");
++ case UnsupportedFaceShapeError:
++ return QStringLiteral("Error: Unsupported face shape in source");
++ case UnsupportedIndexSizeError:
++ return QStringLiteral("Error: Unsupported index size in source");
++ case FileNotFoundError:
++ return QStringLiteral("Error: File not found");
++ case MissingPositionAttributeError:
++ return QStringLiteral("Error: Missing '%1' attribute").arg(
++ QLatin1String(qtPositionAttributeName()));
++ case MissingTextureCoordinateAttributeError:
++ return QStringLiteral("Error: Missing '%1' attribute").arg(
++ QLatin1String(qtTexCoordAttributeName()));
++ case MissingPositionAndTextureCoordinateAttributesError:
++ return QStringLiteral("Error: Missing '%1' and '%2' attributes").arg(
++ QLatin1String(qtPositionAttributeName()),
++ QLatin1String(qtTexCoordAttributeName()));
++ case TooManyAttributesError:
++ return QStringLiteral("Error: Too many attributes");
++ case InvalidPlaneDefinitionError:
++ return QStringLiteral("Error: Invalid plane. "
++ "V and W must be non-null and cannot be parallel");
++ default:
++ return QStringLiteral("Unknown error");
++ };
++ }
++
++ bool QWavefrontMesh::validateAttributes(const QList<QByteArray> &attributes, int *posIndex)
++ {
++ Q_D(QWavefrontMesh);
++ const int attrCount = attributes.size();
++ int positionIndex = attributes.indexOf(qtPositionAttributeName());
++ int texCoordIndex = attributes.indexOf(qtTexCoordAttributeName());
++
++ switch (attrCount) {
++ case 0:
++ d->lastError = NoAttributesError;
++ return false;
++ case 1:
++ if (positionIndex < 0) {
++ d->lastError = MissingPositionAttributeError;
++ return false;
++ }
++ break;
++ case 2:
++ if (positionIndex < 0 || texCoordIndex < 0) {
++ if (positionIndex < 0 && texCoordIndex < 0)
++ d->lastError = MissingPositionAndTextureCoordinateAttributesError;
++ else if (positionIndex < 0)
++ d->lastError = MissingPositionAttributeError;
++ else if (texCoordIndex < 0)
++ d->lastError = MissingTextureCoordinateAttributeError;
++ return false;
++ }
++ break;
++ default:
++ d->lastError = TooManyAttributesError;
++ return false;
++ }
++
++ if (posIndex)
++ *posIndex = positionIndex;
++
++ return true;
++
++ }
++
++ QSGGeometry *QWavefrontMesh::updateGeometry(QSGGeometry *geometry, int attributeCount, int positionIndex,
++ const QRectF &sourceRect, const QRectF &destinationRect)
++ {
++ Q_D(QWavefrontMesh);
++
++ if (geometry == nullptr) {
++ Q_ASSERT(attributeCount == 1 || attributeCount == 2);
++ geometry = new QSGGeometry(attributeCount == 1
++ ? QSGGeometry::defaultAttributes_Point2D()
++ : QSGGeometry::defaultAttributes_TexturedPoint2D(),
++ d->indexes.size(),
++ d->indexes.size(),
++ QSGGeometry::UnsignedShortType);
++ geometry->setDrawingMode(QSGGeometry::DrawTriangles);
++
++ } else {
++ geometry->allocate(d->indexes.size(), d->indexes.size());
++ }
++
++ // If there is not at least a full triangle in the data set, skip out
++ if (d->indexes.size() < 3) {
++ geometry->allocate(0, 0);
++ return geometry;
++ }
++
++ QVector3D planeV = d->planeV;
++ QVector3D planeW = d->planeW;
++
++ // Automatically detect plane based on first face if none is set
++ if (planeV.isNull() || planeW.isNull()) {
++ QVector3D p = d->vertexes.at(d->indexes.at(0).first);
++ planeV = (d->vertexes.at(d->indexes.at(1).first) - p);
++ planeW = (p - d->vertexes.at(d->indexes.at(2).first)).normalized();
++ }
++
++ planeV.normalize();
++ planeW.normalize();
++
++ QVector3D planeNormal = QVector3D::crossProduct(planeV, planeW).normalized();
++ if (planeNormal.isNull()) { // V and W are either parallel or null
++ setLastError(InvalidPlaneDefinitionError);
++ geometry->allocate(0, 0);
++ return geometry;
++ }
++
++ QVector3D planeAxes1 = planeV;
++ QVector3D planeAxes2 = QVector3D::crossProduct(planeAxes1, planeNormal).normalized();
++
++ ushort *indexData = static_cast<ushort *>(geometry->indexData());
++ QSGGeometry::Point2D *vertexData = static_cast<QSGGeometry::Point2D *>(geometry->vertexData());
++
++ float minX = 0.0f;
++ float maxX = 0.0f;
++ float minY = 0.0f;
++ float maxY = 0.0f;
++ for (ushort i = 0; i < ushort(d->indexes.size()); ++i) {
++ *(indexData + i) = i;
++
++ QVector3D v = d->vertexes.at(d->indexes.at(i).first);
++
++ // Project onto plane
++ QVector2D w;
++ v -= QVector3D::dotProduct(planeNormal, v) * planeNormal;
++ w.setX(QVector3D::dotProduct(v, planeAxes1));
++ w.setY(QVector3D::dotProduct(v, planeAxes2));
++
++ QSGGeometry::Point2D *positionData = vertexData + (i * attributeCount + positionIndex);
++ positionData->x = w.x();
++ positionData->y = w.y();
++
++ if (i == 0 || minX > w.x())
++ minX = w.x();
++ if (i == 0 || maxX < w.x())
++ maxX = w.x();
++ if (i == 0 || minY > w.y())
++ minY = w.y();
++ if (i == 0 || maxY < w.y())
++ maxY = w.y();
++
++ if (attributeCount > 1 && !d->textureCoordinates.isEmpty()) {
++ Q_ASSERT(positionIndex == 0 || positionIndex == 1);
++
++ QVector2D uv = d->textureCoordinates.at(d->indexes.at(i).second);
++ QSGGeometry::Point2D *textureCoordinateData = vertexData + (i * attributeCount + (1 - positionIndex));
++ textureCoordinateData->x = uv.x();
++ textureCoordinateData->y = uv.y();
++ }
++ }
++
++ float width = maxX - minX;
++ float height = maxY - minY;
++
++ QVector2D center(minX + width / 2.0f, minY + height / 2.0f);
++ QVector2D scale(1.0f / width, 1.0f / height);
++
++ for (int i = 0; i < geometry->vertexCount(); ++i) {
++ float x = ((vertexData + positionIndex)->x - center.x()) * scale.x();
++ float y = ((vertexData + positionIndex)->y - center.y()) * scale.y();
++
++ for (int attributeIndex = 0; attributeIndex < attributeCount; ++attributeIndex) {
++ if (attributeIndex == positionIndex) {
++ vertexData->x = float(destinationRect.left()) + x * float(destinationRect.width()) + float(destinationRect.width()) / 2.0f;
++ vertexData->y = float(destinationRect.top()) + y * float(destinationRect.height()) + float(destinationRect.height()) / 2.0f;
++ } else {
++ // If there are no texture coordinates, use the normalized vertex
++ float tx = d->textureCoordinates.isEmpty() ? x : vertexData->x;
++ float ty = d->textureCoordinates.isEmpty() ? y : vertexData->y;
++
++ vertexData->x = float(sourceRect.left()) + tx * float(sourceRect.width());
++ vertexData->y = float(sourceRect.top()) + ty * float(sourceRect.height());
++ }
++
++ ++vertexData;
++ }
++ }
++
++ return geometry;
++ }
++
++ /*!
++ \qmlproperty vector3d WavefrontMesh::projectionPlaneV
++
++ Since the Wavefront .obj format describes an object in 3D space, the coordinates
++ have to be projected into 2D before they can be displayed in Qt Quick.
++
++ This will be done in WavefrontMesh by an orthographic projection onto an
++ appropriate plane.
++
++ The projectionPlaneV is one of two vectors in the plane in 3D space. If
++ either this, or \l projectionPlaneW is set to (0, 0, 0) (the default),
++ then the plane will be detected based on the first encountered face in the
++ data set.
++
++ \note projectionPlaneV and \l projectionPlaneW cannot be parallel vectors.
++ */
++ void QWavefrontMesh::setProjectionPlaneV(const QVector3D &v)
++ {
++ Q_D(QWavefrontMesh);
++ if (d->planeV == v)
++ return;
++
++ d->planeV = v;
++ emit projectionPlaneVChanged();
++ }
++
++ QVector3D QWavefrontMesh::projectionPlaneV() const
++ {
++ Q_D(const QWavefrontMesh);
++ return d->planeV;
++ }
++
++ /*!
++ \qmlproperty vector3d WavefrontMesh::projectionPlaneW
++
++ Since the Wavefront .obj format describes an object in 3D space, the coordinates
++ have to be projected into 2D before they can be displayed in Qt Quick.
++
++ This will be done in WavefrontMesh by an orthographic projection onto an
++ appropriate plane.
++
++ The projectionPlaneW is one of two vectors in the plane in 3D space. If
++ either this, or \l projectionPlaneV is set to (0, 0, 0) (the default),
++ then the plane will be detected based on the first encountered face in the
++ data set.
++
++ \note \l projectionPlaneV and projectionPlaneW cannot be parallel vectors.
++ */
++ void QWavefrontMesh::setProjectionPlaneW(const QVector3D &w)
++ {
++ Q_D(QWavefrontMesh);
++ if (d->planeW == w)
++ return;
++
++ d->planeW = w;
++ emit projectionPlaneWChanged();
++ }
++
++ QVector3D QWavefrontMesh::projectionPlaneW() const
++ {
++ Q_D(const QWavefrontMesh);
++ return d->planeW;
++ }
++
++
++ QT_END_NAMESPACE
++
++ #include "moc_qwavefrontmesh_p.cpp"