Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwavefrontmesh.cpp
Go to the documentation of this file.
1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
6
7#include <QtCore/qfile.h>
8#include <QtCore/qtextstream.h>
9#include <QtCore/private/qobject_p.h>
10
11#include <QtGui/qvector2d.h>
12#include <QtGui/qvector3d.h>
13
14#include <QtQml/qqmlfile.h>
15#include <QtQml/qqmlcontext.h>
16
17#include <QtQuick/qsggeometry.h>
18
20
50
51/*!
52 \qmlmodule Qt.labs.wavefrontmesh 1.\QtMinorVersion
53 \title Qt Labs WavefrontMesh QML Types
54 \ingroup qmlmodules
55 \brief The WavefrontMesh provides a mesh based on a Wavefront .obj file.
56
57 To use this module, import the module with the following line:
58
59 \qml
60 import Qt.labs.wavefrontmesh
61 \endqml
62*/
63
64/*!
65 \qmltype WavefrontMesh
66 \inqmlmodule Qt.labs.wavefrontmesh
67//! \nativetype QWavefrontMesh
68 \ingroup qtquick-effects
69 \brief The WavefrontMesh provides a mesh based on a Wavefront .obj file.
70 \since 5.12
71
72 WavefrontMesh reads the geometry from a Wavefront .obj file and generates
73 a two-dimensional \l{QSGGeometry}{geometry} from this. If the .obj file
74 contains a three-dimensional shape, it will be orthographically projected,
75 onto a plane. If defined, this is given by \l projectionPlaneV
76 and \l projectionPlaneW. Otherwise, the first face encountered in the data
77 will be used to determine the projection plane.
78
79 If the file contains texture coordinates, these will also be used. Otherwise,
80 the vertexes of the object will be normalized and used.
81
82 The mesh can be used in a ShaderEffect to define the shaded geometry. The
83 geometry will be normalized before use, so the position and scale of the
84 input objects have no impact on the result.
85
86 \note Some Wavefront exporters will change the source scene's coordinate system
87 before exporting it. This can cause unexpected results when Qt applies the
88 projection. If the visual results are not as you expect, try checking the export
89 parameters and the documentation of the editor tool to see if this is the case.
90
91 For instance, the following example takes an .obj file containing a standard torus
92 and visualizes the automatically generated texture coordinates.
93
94 \table
95 \row
96 \li \image qtlabs-wavefrontmesh.png
97 \li \qml
98 import QtQuick 2.\1
99 import Qt.labs.wavefrontmesh 1.\1
100
101 ShaderEffect {
102 width: 200
103 height: 200
104 mesh: WavefrontMesh {
105 source: "torus.obj"
106 projectionPlaneV: Qt.vector3d(0, 1, 0)
107 projectionPlaneW: Qt.vector3d(1, 0, 0)
108 }
109 vertexShader: "
110 uniform highp mat4 qt_Matrix;
111 attribute highp vec4 qt_Vertex;
112 attribute highp vec2 qt_MultiTexCoord0;
113 varying highp vec2 coord;
114 void main() {
115 coord = qt_MultiTexCoord0;
116 gl_Position = qt_Matrix * qt_Vertex;
117 }"
118 fragmentShader: "
119 varying highp vec2 coord;
120 uniform lowp float qt_Opacity;
121 void main() {
122 gl_FragColor = vec4(coord.x, coord.y, 0.0, 1.0);
123 }"
124
125 }
126 \endqml
127 \endtable
128
129 \note Since the input is a 3D torus, we need to define the projection plane. This would not be
130necessary when using a 2D shape as input. We use the XY plane in this case, because of the
131orientation of the input.
132*/
133
134QWavefrontMesh::QWavefrontMesh(QObject *parent)
135 : QQuickShaderEffectMesh(*(new QWavefrontMeshPrivate), parent)
136{
137 connect(this, &QWavefrontMesh::sourceChanged, this, &QWavefrontMesh::readData);
138 connect(this, &QWavefrontMesh::projectionPlaneVChanged, this, &QQuickShaderEffectMesh::geometryChanged);
139 connect(this, &QWavefrontMesh::projectionPlaneWChanged, this, &QQuickShaderEffectMesh::geometryChanged);
140}
141
142QWavefrontMesh::~QWavefrontMesh()
143{
144}
145
146/*!
147 \qmlproperty enumeration WavefrontMesh::lastError
148
149 This property holds the last error, if any, that occurred when parsing the
150 source or building the mesh.
151
152 \list
153 \li WavefrontMesh.NoError No error has occurred.
154 \li WavefrontMesh.InvalidSourceError The source was not recognized as a valid .obj file.
155 \li WavefrontMesh.UnsupportedFaceShapeError The faces in the source is of an unsupported type.
156 WavefrontMesh only supports triangles and convex quads.
157 \li WavefrontMesh.UnsupportedIndexSizeError The source shape is too large. Only 16 bit indexes are supported.
158 \li WavefrontMesh.FileNotFoundError The source file was not found.
159 \li WavefrontMesh.MissingPositionAttributeError The 'qt_Vertex' attribute is missing from the shaders.
160 \li WavefrontMesh.MissingTextureCoordinateAttributeError The texture coordinate attribute in the shaders is wrongly named. Use 'qt_MultiTexCoord0'.
161 \li WavefrontMesh.MissingPositionAndTextureCoordinateAttributesError Both the 'qt_Vertex' and 'qt_MultiTexCoord0' attributes are missing from the shaders.
162 \li WavefrontMesh.TooManyAttributesError The shaders expect too many attributes (maximum is two: Position, 'qt_Vertex', and texture coordinate, 'qt_MultiTexCoord0').
163 \li WavefrontMesh.InvalidPlaneDefinitionError The V and W vectors in the plane cannot be null, nor parallel to each other.
164 \endlist
165*/
166
167QWavefrontMesh::Error QWavefrontMesh::lastError() const
168{
169 Q_D(const QWavefrontMesh);
170 return d->lastError;
171}
172
173void QWavefrontMesh::setLastError(Error lastError)
174{
175 Q_D(QWavefrontMesh);
176 if (d->lastError == lastError)
177 return;
178
179 d->lastError = lastError;
180 emit lastErrorChanged();
181}
182
183/*!
184 \qmlproperty url WavefrontMesh::source
185
186 This property holds the URL of the source. This must be either a local file or in qrc. The source will
187 be read as a Wavefront .obj file and the geometry will be updated.
188*/
189QUrl QWavefrontMesh::source() const
190{
191 Q_D(const QWavefrontMesh);
192 return d->source;
193}
194
195void QWavefrontMesh::setSource(const QUrl &source)
196{
197 Q_D(QWavefrontMesh);
198 if (d->source == source)
199 return;
200
201 d->source = source;
202 emit sourceChanged();
203}
204
205void QWavefrontMesh::readData()
206{
207 Q_D(QWavefrontMesh);
208 d->vertexes.clear();
209 d->textureCoordinates.clear();
210 d->indexes.clear();
211
212 QString localFile = QQmlFile::urlToLocalFileOrQrc(d->source);
213 if (!localFile.isEmpty()) {
214 QFile file(localFile);
215 if (file.open(QIODevice::ReadOnly)) {
216 QTextStream stream(&file);
217
218 QString buffer;
219 buffer.reserve(256);
220
221 static QChar space(QLatin1Char(' '));
222 static QChar slash(QLatin1Char('/'));
223
224 while (!stream.atEnd()) {
225 stream.readLineInto(&buffer);
226 auto tokens = QStringView{buffer}.split(space, Qt::SkipEmptyParts);
227 if (tokens.size() < 2)
228 continue;
229
230 QByteArray command = tokens.at(0).toLatin1();
231
232 if (command == "vt") {
233 bool ok;
234 float u = tokens.at(1).toFloat(&ok);
235 if (!ok) {
236 setLastError(InvalidSourceError);
237 return;
238 }
239
240 float v = tokens.size() > 2 ? tokens.at(2).toFloat(&ok) : 0.0;
241 if (!ok) {
242 setLastError(InvalidSourceError);
243 return;
244 }
245
246 d->textureCoordinates.append(QVector2D(u, v));
247 } else if (command == "v") {
248 // Format: v <x> <y> <z> [w]
249 if (tokens.size() < 4 || tokens.size() > 5) {
250 setLastError(InvalidSourceError);
251 return;
252 }
253
254 bool ok;
255
256 float x = tokens.at(1).toFloat(&ok);
257 if (!ok) {
258 setLastError(InvalidSourceError);
259 return;
260 }
261
262 float y = tokens.at(2).toFloat(&ok);
263 if (!ok) {
264 setLastError(InvalidSourceError);
265 return;
266 }
267
268 float z = tokens.at(3).toFloat(&ok);
269 if (!ok) {
270 setLastError(InvalidSourceError);
271 return;
272 }
273
274 d->vertexes.append(QVector3D(x, y, z));
275 } else if (command == "f") {
276 // The scenegraph only supports triangles, so we
277 // support triangles and quads (which we split up)
278 int p1, p2, p3;
279 int t1 = 0;
280 int t2 = 0;
281 int t3 = 0;
282 if (tokens.size() >= 4 && tokens.size() <= 5) {
283 {
284 bool ok;
285 auto faceTokens = tokens.at(1).split(slash, Qt::SkipEmptyParts);
286 Q_ASSERT(!faceTokens.isEmpty());
287
288 p1 = faceTokens.at(0).toInt(&ok) - 1;
289 if (!ok) {
290 setLastError(InvalidSourceError);
291 return;
292 }
293
294 if (faceTokens.size() > 1) {
295 t1 = faceTokens.at(1).toInt(&ok) - 1;
296 if (!ok) {
297 setLastError(InvalidSourceError);
298 return;
299 }
300 }
301 }
302
303 {
304 bool ok;
305 auto faceTokens = tokens.at(2).split(slash, Qt::SkipEmptyParts);
306 Q_ASSERT(!faceTokens.isEmpty());
307
308 p2 = faceTokens.at(0).toInt(&ok) - 1;
309 if (!ok) {
310 setLastError(InvalidSourceError);
311 return;
312 }
313
314 if (faceTokens.size() > 1) {
315 t2 = faceTokens.at(1).toInt(&ok) - 1;
316 if (!ok) {
317 setLastError(InvalidSourceError);
318 return;
319 }
320 }
321 }
322
323 {
324 bool ok;
325 auto faceTokens = tokens.at(3).split(slash, Qt::SkipEmptyParts);
326 Q_ASSERT(!faceTokens.isEmpty());
327
328 p3 = faceTokens.at(0).toInt(&ok) - 1;
329 if (!ok) {
330 setLastError(InvalidSourceError);
331 return;
332 }
333
334 if (faceTokens.size() > 1) {
335 t3 = faceTokens.at(1).toInt(&ok) - 1;
336 if (!ok) {
337 setLastError(InvalidSourceError);
338 return;
339 }
340 }
341 }
342
343 if (Q_UNLIKELY(p1 < 0 || p1 > UINT16_MAX
344 || p2 < 0 || p2 > UINT16_MAX
345 || p3 < 0 || p3 > UINT16_MAX
346 || t1 < 0 || t1 > UINT16_MAX
347 || t2 < 0 || t2 > UINT16_MAX
348 || t3 < 0 || t3 > UINT16_MAX)) {
349 setLastError(UnsupportedIndexSizeError);
350 return;
351 }
352
353 d->indexes.append(std::make_pair(ushort(p1), ushort(t1)));
354 d->indexes.append(std::make_pair(ushort(p2), ushort(t2)));
355 d->indexes.append(std::make_pair(ushort(p3), ushort(t3)));
356 } else {
357 setLastError(UnsupportedFaceShapeError);
358 return;
359 }
360
361 if (tokens.size() == 5) {
362 bool ok;
363 auto faceTokens = tokens.at(4).split(slash, Qt::SkipEmptyParts);
364 Q_ASSERT(!faceTokens.isEmpty());
365
366 int p4 = faceTokens.at(0).toInt(&ok) - 1;
367 if (!ok) {
368 setLastError(InvalidSourceError);
369 return;
370 }
371
372 int t4 = 0;
373 if (faceTokens.size() > 1) {
374 t4 = faceTokens.at(1).toInt(&ok) - 1;
375 if (!ok) {
376 setLastError(InvalidSourceError);
377 return;
378 }
379 }
380
381 if (Q_UNLIKELY(p4 < 0 || p4 > UINT16_MAX || t4 < 0 || t4 > UINT16_MAX)) {
382 setLastError(UnsupportedIndexSizeError);
383 return;
384 }
385
386 // ### Assumes convex quad, correct algorithm is to find the concave corner,
387 // and if there is one, do the split on the line between this and the corner it is
388 // not connected to. Also assumes order of vertices is counter clockwise.
389 d->indexes.append(std::make_pair(ushort(p3), ushort(t3)));
390 d->indexes.append(std::make_pair(ushort(p4), ushort(t4)));
391 d->indexes.append(std::make_pair(ushort(p1), ushort(t1)));
392 }
393 }
394 }
395 } else {
396 setLastError(FileNotFoundError);
397 }
398 } else {
399 setLastError(InvalidSourceError);
400 }
401
402 emit geometryChanged();
403}
404
405QString QWavefrontMesh::log() const
406{
407 Q_D(const QWavefrontMesh);
408 switch (d->lastError) {
409 case NoError:
410 return QStringLiteral("No error");
411 case InvalidSourceError:
412 return QStringLiteral("Error: Invalid source");
413 case UnsupportedFaceShapeError:
414 return QStringLiteral("Error: Unsupported face shape in source");
415 case UnsupportedIndexSizeError:
416 return QStringLiteral("Error: Unsupported index size in source");
417 case FileNotFoundError:
418 return QStringLiteral("Error: File not found");
419 case MissingPositionAttributeError:
420 return QStringLiteral("Error: Missing '%1' attribute").arg(
421 QLatin1String(qtPositionAttributeName()));
422 case MissingTextureCoordinateAttributeError:
423 return QStringLiteral("Error: Missing '%1' attribute").arg(
424 QLatin1String(qtTexCoordAttributeName()));
425 case MissingPositionAndTextureCoordinateAttributesError:
426 return QStringLiteral("Error: Missing '%1' and '%2' attributes").arg(
427 QLatin1String(qtPositionAttributeName()),
428 QLatin1String(qtTexCoordAttributeName()));
429 case TooManyAttributesError:
430 return QStringLiteral("Error: Too many attributes");
431 case InvalidPlaneDefinitionError:
432 return QStringLiteral("Error: Invalid plane. "
433 "V and W must be non-null and cannot be parallel");
434 default:
435 return QStringLiteral("Unknown error");
436 };
437}
438
439bool QWavefrontMesh::validateAttributes(const QList<QByteArray> &attributes, int *posIndex)
440{
441 Q_D(QWavefrontMesh);
442 const int attrCount = attributes.size();
443 int positionIndex = attributes.indexOf(qtPositionAttributeName());
444 int texCoordIndex = attributes.indexOf(qtTexCoordAttributeName());
445
446 switch (attrCount) {
447 case 0:
448 d->lastError = NoAttributesError;
449 return false;
450 case 1:
451 if (positionIndex < 0) {
452 d->lastError = MissingPositionAttributeError;
453 return false;
454 }
455 break;
456 case 2:
457 if (positionIndex < 0 || texCoordIndex < 0) {
458 if (positionIndex < 0 && texCoordIndex < 0)
459 d->lastError = MissingPositionAndTextureCoordinateAttributesError;
460 else if (positionIndex < 0)
461 d->lastError = MissingPositionAttributeError;
462 else if (texCoordIndex < 0)
463 d->lastError = MissingTextureCoordinateAttributeError;
464 return false;
465 }
466 break;
467 default:
468 d->lastError = TooManyAttributesError;
469 return false;
470 }
471
472 if (posIndex)
473 *posIndex = positionIndex;
474
475 return true;
476
477}
478
479QSGGeometry *QWavefrontMesh::updateGeometry(QSGGeometry *geometry, int attributeCount, int positionIndex,
480 const QRectF &sourceRect, const QRectF &destinationRect)
481{
482 Q_D(QWavefrontMesh);
483
484 if (geometry == nullptr) {
485 Q_ASSERT(attributeCount == 1 || attributeCount == 2);
486 geometry = new QSGGeometry(attributeCount == 1
487 ? QSGGeometry::defaultAttributes_Point2D()
488 : QSGGeometry::defaultAttributes_TexturedPoint2D(),
489 d->indexes.size(),
490 d->indexes.size(),
491 QSGGeometry::UnsignedShortType);
492 geometry->setDrawingMode(QSGGeometry::DrawTriangles);
493
494 } else {
495 geometry->allocate(d->indexes.size(), d->indexes.size());
496 }
497
498 // If there is not at least a full triangle in the data set, skip out
499 if (d->indexes.size() < 3) {
500 geometry->allocate(0, 0);
501 return geometry;
502 }
503
504 QVector3D planeV = d->planeV;
505 QVector3D planeW = d->planeW;
506
507 // Automatically detect plane based on first face if none is set
508 if (planeV.isNull() || planeW.isNull()) {
509 QVector3D p = d->vertexes.at(d->indexes.at(0).first);
510 planeV = (d->vertexes.at(d->indexes.at(1).first) - p);
511 planeW = (p - d->vertexes.at(d->indexes.at(2).first)).normalized();
512 }
513
514 planeV.normalize();
515 planeW.normalize();
516
517 QVector3D planeNormal = QVector3D::crossProduct(planeV, planeW).normalized();
518 if (planeNormal.isNull()) { // V and W are either parallel or null
519 setLastError(InvalidPlaneDefinitionError);
520 geometry->allocate(0, 0);
521 return geometry;
522 }
523
524 QVector3D planeAxes1 = planeV;
525 QVector3D planeAxes2 = QVector3D::crossProduct(planeAxes1, planeNormal).normalized();
526
527 ushort *indexData = static_cast<ushort *>(geometry->indexData());
528 QSGGeometry::Point2D *vertexData = static_cast<QSGGeometry::Point2D *>(geometry->vertexData());
529
530 float minX = 0.0f;
531 float maxX = 0.0f;
532 float minY = 0.0f;
533 float maxY = 0.0f;
534 for (ushort i = 0; i < ushort(d->indexes.size()); ++i) {
535 *(indexData + i) = i;
536
537 QVector3D v = d->vertexes.at(d->indexes.at(i).first);
538
539 // Project onto plane
540 QVector2D w;
541 v -= QVector3D::dotProduct(planeNormal, v) * planeNormal;
542 w.setX(QVector3D::dotProduct(v, planeAxes1));
543 w.setY(QVector3D::dotProduct(v, planeAxes2));
544
545 QSGGeometry::Point2D *positionData = vertexData + (i * attributeCount + positionIndex);
546 positionData->x = w.x();
547 positionData->y = w.y();
548
549 if (i == 0 || minX > w.x())
550 minX = w.x();
551 if (i == 0 || maxX < w.x())
552 maxX = w.x();
553 if (i == 0 || minY > w.y())
554 minY = w.y();
555 if (i == 0 || maxY < w.y())
556 maxY = w.y();
557
558 if (attributeCount > 1 && !d->textureCoordinates.isEmpty()) {
559 Q_ASSERT(positionIndex == 0 || positionIndex == 1);
560
561 QVector2D uv = d->textureCoordinates.at(d->indexes.at(i).second);
562 QSGGeometry::Point2D *textureCoordinateData = vertexData + (i * attributeCount + (1 - positionIndex));
563 textureCoordinateData->x = uv.x();
564 textureCoordinateData->y = uv.y();
565 }
566 }
567
568 float width = maxX - minX;
569 float height = maxY - minY;
570
571 QVector2D center(minX + width / 2.0f, minY + height / 2.0f);
572 QVector2D scale(1.0f / width, 1.0f / height);
573
574 for (int i = 0; i < geometry->vertexCount(); ++i) {
575 float x = ((vertexData + positionIndex)->x - center.x()) * scale.x();
576 float y = ((vertexData + positionIndex)->y - center.y()) * scale.y();
577
578 for (int attributeIndex = 0; attributeIndex < attributeCount; ++attributeIndex) {
579 if (attributeIndex == positionIndex) {
580 vertexData->x = float(destinationRect.left()) + x * float(destinationRect.width()) + float(destinationRect.width()) / 2.0f;
581 vertexData->y = float(destinationRect.top()) + y * float(destinationRect.height()) + float(destinationRect.height()) / 2.0f;
582 } else {
583 // If there are no texture coordinates, use the normalized vertex
584 float tx = d->textureCoordinates.isEmpty() ? x : vertexData->x;
585 float ty = d->textureCoordinates.isEmpty() ? y : vertexData->y;
586
587 vertexData->x = float(sourceRect.left()) + tx * float(sourceRect.width());
588 vertexData->y = float(sourceRect.top()) + ty * float(sourceRect.height());
589 }
590
591 ++vertexData;
592 }
593 }
594
595 return geometry;
596}
597
598/*!
599 \qmlproperty vector3d WavefrontMesh::projectionPlaneV
600
601 Since the Wavefront .obj format describes an object in 3D space, the coordinates
602 have to be projected into 2D before they can be displayed in Qt Quick.
603
604 This will be done in WavefrontMesh by an orthographic projection onto an
605 appropriate plane.
606
607 The projectionPlaneV is one of two vectors in the plane in 3D space. If
608 either this, or \l projectionPlaneW is set to (0, 0, 0) (the default),
609 then the plane will be detected based on the first encountered face in the
610 data set.
611
612 \note projectionPlaneV and \l projectionPlaneW cannot be parallel vectors.
613*/
614void QWavefrontMesh::setProjectionPlaneV(const QVector3D &v)
615{
616 Q_D(QWavefrontMesh);
617 if (d->planeV == v)
618 return;
619
620 d->planeV = v;
621 emit projectionPlaneVChanged();
622}
623
624QVector3D QWavefrontMesh::projectionPlaneV() const
625{
626 Q_D(const QWavefrontMesh);
627 return d->planeV;
628}
629
630/*!
631 \qmlproperty vector3d WavefrontMesh::projectionPlaneW
632
633 Since the Wavefront .obj format describes an object in 3D space, the coordinates
634 have to be projected into 2D before they can be displayed in Qt Quick.
635
636 This will be done in WavefrontMesh by an orthographic projection onto an
637 appropriate plane.
638
639 The projectionPlaneW is one of two vectors in the plane in 3D space. If
640 either this, or \l projectionPlaneV is set to (0, 0, 0) (the default),
641 then the plane will be detected based on the first encountered face in the
642 data set.
643
644 \note \l projectionPlaneV and projectionPlaneW cannot be parallel vectors.
645*/
646void QWavefrontMesh::setProjectionPlaneW(const QVector3D &w)
647{
648 Q_D(QWavefrontMesh);
649 if (d->planeW == w)
650 return;
651
652 d->planeW = w;
653 emit projectionPlaneWChanged();
654}
655
656QVector3D QWavefrontMesh::projectionPlaneW() const
657{
658 Q_D(const QWavefrontMesh);
659 return d->planeW;
660}
661
662
663QT_END_NAMESPACE
664
665#include "moc_qwavefrontmesh_p.cpp"
QVector< QVector3D > vertexes
QVector< QVector2D > textureCoordinates