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

source code of qtdeclarative/src/labs/wavefrontmesh/qwavefrontmesh.cpp