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