1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qssgqmlutilities_p.h"
5#include "qssgscenedesc_p.h"
6
7#include <QVector2D>
8#include <QVector3D>
9#include <QVector4D>
10#include <QQuaternion>
11#include <QDebug>
12#include <QRegularExpression>
13#include <QtCore/qdir.h>
14#include <QtCore/qfile.h>
15#include <QtCore/qbuffer.h>
16
17#include <QtGui/qimage.h>
18#include <QtGui/qimagereader.h>
19
20#include <QtQuick3DUtils/private/qssgmesh_p.h>
21#include <QtQuick3DUtils/private/qssgassert_p.h>
22
23#include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
24
25#ifdef QT_QUICK3D_ENABLE_RT_ANIMATIONS
26#include <QtCore/QCborStreamWriter>
27#include <QtQuickTimeline/private/qquicktimeline_p.h>
28#endif // QT_QUICK3D_ENABLE_RT_ANIMATIONS
29
30QT_BEGIN_NAMESPACE
31
32using namespace Qt::StringLiterals;
33
34namespace QSSGQmlUtilities {
35
36class PropertyMap
37{
38public:
39 typedef QHash<QByteArray, QVariant> PropertiesMap;
40
41 static PropertyMap *instance();
42
43 PropertiesMap propertiesForType(QSSGSceneDesc::Node::RuntimeType type);
44 QVariant getDefaultValue(QSSGSceneDesc::Node::RuntimeType type, const char *property);
45 bool isDefaultValue(QSSGSceneDesc::Node::RuntimeType type, const char *property, const QVariant &value);
46
47private:
48 PropertyMap();
49
50 QHash<QSSGSceneDesc::Node::RuntimeType, PropertiesMap> m_properties;
51
52};
53
54QString qmlComponentName(const QString &name) {
55 QString nameCopy = name;
56 if (nameCopy.isEmpty())
57 return QStringLiteral("Presentation");
58
59 nameCopy = sanitizeQmlId(id: nameCopy);
60
61 if (nameCopy[0].isLower())
62 nameCopy[0] = nameCopy[0].toUpper();
63
64 return nameCopy;
65}
66
67QString colorToQml(const QColor &color) {
68 QString colorString;
69 colorString = QLatin1Char('\"') + color.name(format: QColor::HexArgb) + QLatin1Char('\"');
70 return colorString;
71}
72
73QString variantToQml(const QVariant &variant) {
74 switch (variant.typeId()) {
75 case QMetaType::Float: {
76 auto value = variant.toDouble();
77 return QString::number(value);
78 }
79 case QMetaType::QVector2D: {
80 auto value = variant.value<QVector2D>();
81 return QString(QStringLiteral("Qt.vector2d(") + QString::number(double(value.x())) +
82 QStringLiteral(", ") + QString::number(double(value.y())) +
83 QStringLiteral(")"));
84 }
85 case QMetaType::QVector3D: {
86 auto value = variant.value<QVector3D>();
87 return QString(QStringLiteral("Qt.vector3d(") + QString::number(double(value.x())) +
88 QStringLiteral(", ") + QString::number(double(value.y())) +
89 QStringLiteral(", ") + QString::number(double(value.z())) +
90 QStringLiteral(")"));
91 }
92 case QMetaType::QVector4D: {
93 auto value = variant.value<QVector4D>();
94 return QString(QStringLiteral("Qt.vector4d(") + QString::number(double(value.x())) +
95 QStringLiteral(", ") + QString::number(double(value.y())) +
96 QStringLiteral(", ") + QString::number(double(value.z())) +
97 QStringLiteral(", ") + QString::number(double(value.w())) +
98 QStringLiteral(")"));
99 }
100 case QMetaType::QColor: {
101 auto value = variant.value<QColor>();
102 return colorToQml(color: value);
103 }
104 case QMetaType::QQuaternion: {
105 auto value = variant.value<QQuaternion>();
106 return QString(QStringLiteral("Qt.quaternion(") + QString::number(double(value.scalar())) +
107 QStringLiteral(", ") + QString::number(double(value.x())) +
108 QStringLiteral(", ") + QString::number(double(value.y())) +
109 QStringLiteral(", ") + QString::number(double(value.z())) +
110 QStringLiteral(")"));
111 }
112 default:
113 return variant.toString();
114 }
115}
116
117QString sanitizeQmlId(const QString &id)
118{
119 QString idCopy = id;
120 // If the id starts with a number...
121 if (!idCopy.isEmpty() && idCopy.at(i: 0).isNumber())
122 idCopy.prepend(QStringLiteral("node"));
123
124 // sometimes first letter is a # (don't replace with underscore)
125 if (idCopy.startsWith(c: QChar::fromLatin1(c: '#')))
126 idCopy.remove(i: 0, len: 1);
127
128 // Replace all the characters other than ascii letters, numbers or underscore to underscores.
129 static QRegularExpression regExp(QStringLiteral("\\W"));
130 idCopy.replace(re: regExp, QStringLiteral("_"));
131
132 // first letter of id can not be upper case
133 // to make it look nicer, lower-case the initial run of all-upper-case characters
134 if (!idCopy.isEmpty() && idCopy[0].isUpper()) {
135
136 int i = 0;
137 int len = idCopy.length();
138 while (i < len && idCopy[i].isUpper()) {
139 idCopy[i] = idCopy[i].toLower();
140 ++i;
141 }
142 }
143
144 // ### qml keywords as names
145 static QSet<QByteArray> keywords {
146 "x",
147 "y",
148 "as",
149 "do",
150 "if",
151 "in",
152 "on",
153 "of",
154 "for",
155 "get",
156 "int",
157 "let",
158 "new",
159 "set",
160 "try",
161 "var",
162 "top",
163 "byte",
164 "case",
165 "char",
166 "else",
167 "num",
168 "from",
169 "goto",
170 "null",
171 "this",
172 "true",
173 "void",
174 "with",
175 "clip",
176 "item",
177 "flow",
178 "font",
179 "text",
180 "left",
181 "data",
182 "alias",
183 "break",
184 "state",
185 "scale",
186 "color",
187 "right",
188 "catch",
189 "class",
190 "const",
191 "false",
192 "float",
193 "layer", // Design Studio doesn't like "layer" as an id
194 "short",
195 "super",
196 "throw",
197 "while",
198 "yield",
199 "border",
200 "source",
201 "delete",
202 "double",
203 "export",
204 "import",
205 "native",
206 "public",
207 "pragma",
208 "return",
209 "signal",
210 "static",
211 "switch",
212 "throws",
213 "bottom",
214 "parent",
215 "typeof",
216 "boolean",
217 "opacity",
218 "enabled",
219 "anchors",
220 "padding",
221 "default",
222 "extends",
223 "finally",
224 "package",
225 "private",
226 "abstract",
227 "continue",
228 "debugger",
229 "function",
230 "property",
231 "readonly",
232 "children",
233 "volatile",
234 "interface",
235 "protected",
236 "transient",
237 "implements",
238 "instanceof",
239 "synchronized"
240 };
241 if (keywords.contains(value: idCopy.toUtf8())) {
242 idCopy += QStringLiteral("_");
243 }
244
245 // We may have removed all the characters by now
246 if (idCopy.isEmpty())
247 idCopy = QStringLiteral("node");
248
249 return idCopy;
250}
251
252QString sanitizeQmlSourcePath(const QString &source, bool removeParentDirectory)
253{
254 QString sourceCopy = source;
255
256 if (removeParentDirectory)
257 sourceCopy = QSSGQmlUtilities::stripParentDirectory(filePath: sourceCopy);
258
259 sourceCopy.replace(before: QChar::fromLatin1(c: '\\'), after: QChar::fromLatin1(c: '/'));
260
261 // must be surrounded in quotes
262 return QString(QStringLiteral("\"") + sourceCopy + QStringLiteral("\""));
263}
264
265PropertyMap *PropertyMap::instance()
266{
267 static PropertyMap p;
268 return &p;
269}
270
271PropertyMap::PropertiesMap PropertyMap::propertiesForType(QSSGSceneDesc::Node::RuntimeType type)
272{
273 return m_properties[type];
274}
275
276QVariant PropertyMap::getDefaultValue(QSSGSceneDesc::Node::RuntimeType type, const char *property)
277{
278 QVariant value;
279
280 if (m_properties.contains(key: type)) {
281 auto properties = m_properties[type];
282 value = properties.value(key: property);
283 }
284
285 return value;
286}
287
288bool PropertyMap::isDefaultValue(QSSGSceneDesc::Node::RuntimeType type, const char *property, const QVariant &value)
289{
290 bool isTheSame = value == getDefaultValue(type, property);
291 return isTheSame;
292}
293
294static PropertyMap::PropertiesMap getObjectPropertiesMap(QObject *object) {
295 PropertyMap::PropertiesMap propertiesMap;
296 auto metaObject = object->metaObject();
297 for (auto i = 0; i < metaObject->propertyCount(); ++i) {
298 auto property = metaObject->property(index: i);
299 const auto name = property.name();
300 const auto value = property.read(obj: object);
301 propertiesMap.insert(key: name, value);
302 }
303 return propertiesMap;
304}
305
306PropertyMap::PropertyMap()
307{
308 // Create a table containing the default values for each property for each supported type
309 {
310 QQuick3DNode node;
311 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Node, value: getObjectPropertiesMap(object: &node));
312 }
313 {
314 QQuick3DPrincipledMaterial principledMaterial;
315 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::PrincipledMaterial, value: getObjectPropertiesMap(object: &principledMaterial));
316 }
317 {
318 QQuick3DSpecularGlossyMaterial specularGlossyMaterial;
319 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::SpecularGlossyMaterial, value: getObjectPropertiesMap(object: &specularGlossyMaterial));
320 }
321 {
322 QQuick3DCustomMaterial customMaterial;
323 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::CustomMaterial, value: getObjectPropertiesMap(object: &customMaterial));
324 }
325 {
326 QQuick3DTexture texture;
327 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Image2D, value: getObjectPropertiesMap(object: &texture));
328 }
329 {
330 QQuick3DCubeMapTexture cubeMapTexture;
331 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::ImageCube, value: getObjectPropertiesMap(object: &cubeMapTexture));
332 }
333 {
334 QQuick3DTextureData textureData;
335 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::TextureData, value: getObjectPropertiesMap(object: &textureData));
336 }
337 {
338 QQuick3DModel model;
339 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Model, value: getObjectPropertiesMap(object: &model));
340 }
341 {
342 QQuick3DOrthographicCamera orthographicCamera;
343 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::OrthographicCamera, value: getObjectPropertiesMap(object: &orthographicCamera));
344 }
345 {
346 QQuick3DPerspectiveCamera perspectiveCamera;
347 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::PerspectiveCamera, value: getObjectPropertiesMap(object: &perspectiveCamera));
348 }
349 {
350 QQuick3DDirectionalLight directionalLight;
351 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::DirectionalLight, value: getObjectPropertiesMap(object: &directionalLight));
352 }
353 {
354 QQuick3DPointLight pointLight;
355 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::PointLight, value: getObjectPropertiesMap(object: &pointLight));
356 }
357 {
358 QQuick3DSpotLight spotLight;
359 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::SpotLight, value: getObjectPropertiesMap(object: &spotLight));
360 }
361 {
362 QQuick3DSkeleton skeleton;
363 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Skeleton, value: getObjectPropertiesMap(object: &skeleton));
364 }
365 {
366 QQuick3DJoint joint;
367 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Joint, value: getObjectPropertiesMap(object: &joint));
368 }
369 {
370 QQuick3DSkin skin;
371 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Skin, value: getObjectPropertiesMap(object: &skin));
372 }
373 {
374 QQuick3DMorphTarget morphTarget;
375 m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::MorphTarget, value: getObjectPropertiesMap(object: &morphTarget));
376 }
377}
378
379struct OutputContext
380{
381 enum Type : quint8 { Header, RootNode, NodeTree, Resource };
382 enum Options : quint8
383 {
384 None,
385 ExpandValueComponents = 0x1,
386 DesignStudioWorkarounds = ExpandValueComponents | 0x2
387 };
388 QTextStream &stream;
389 QDir outdir;
390 QString sourceDir;
391 quint8 indent = 0;
392 Type type = NodeTree;
393 quint8 options = Options::None;
394 quint16 scopeDepth = 0;
395};
396
397template<QSSGSceneDesc::Material::RuntimeType T>
398const char *qmlElementName() { static_assert(!std::is_same_v<decltype(T), decltype(T)>, "Unknown type"); return nullptr; }
399template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::Node>() { return "Node"; }
400
401template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::SpecularGlossyMaterial>() { return "SpecularGlossyMaterial"; }
402template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::PrincipledMaterial>() { return "PrincipledMaterial"; }
403template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::CustomMaterial>() { return "CustomMaterial"; }
404template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::OrthographicCamera>() { return "OrthographicCamera"; }
405template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::PerspectiveCamera>() { return "PerspectiveCamera"; }
406
407template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::Model>() { return "Model"; }
408
409template<> const char *qmlElementName<QSSGSceneDesc::Texture::RuntimeType::Image2D>() { return "Texture"; }
410template<> const char *qmlElementName<QSSGSceneDesc::Texture::RuntimeType::ImageCube>() { return "CubeMapTexture"; }
411template<> const char *qmlElementName<QSSGSceneDesc::Texture::RuntimeType::TextureData>() { return "TextureData"; }
412
413template<> const char *qmlElementName<QSSGSceneDesc::Camera::RuntimeType::DirectionalLight>() { return "DirectionalLight"; }
414template<> const char *qmlElementName<QSSGSceneDesc::Camera::RuntimeType::SpotLight>() { return "SpotLight"; }
415template<> const char *qmlElementName<QSSGSceneDesc::Camera::RuntimeType::PointLight>() { return "PointLight"; }
416
417template<> const char *qmlElementName<QSSGSceneDesc::Joint::RuntimeType::Joint>() { return "Joint"; }
418template<> const char *qmlElementName<QSSGSceneDesc::Skeleton::RuntimeType::Skeleton>() { return "Skeleton"; }
419template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::Skin>() { return "Skin"; }
420template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::MorphTarget>() { return "MorphTarget"; }
421
422static const char *getQmlElementName(const QSSGSceneDesc::Node &node)
423{
424 using RuntimeType = QSSGSceneDesc::Node::RuntimeType;
425 switch (node.runtimeType) {
426 case RuntimeType::Node:
427 return qmlElementName<RuntimeType::Node>();
428 case RuntimeType::PrincipledMaterial:
429 return qmlElementName<RuntimeType::PrincipledMaterial>();
430 case RuntimeType::SpecularGlossyMaterial:
431 return qmlElementName<RuntimeType::SpecularGlossyMaterial>();
432 case RuntimeType::CustomMaterial:
433 return qmlElementName<RuntimeType::CustomMaterial>();
434 case RuntimeType::Image2D:
435 return qmlElementName<RuntimeType::Image2D>();
436 case RuntimeType::ImageCube:
437 return qmlElementName<RuntimeType::ImageCube>();
438 case RuntimeType::TextureData:
439 return qmlElementName<RuntimeType::TextureData>();
440 case RuntimeType::Model:
441 return qmlElementName<RuntimeType::Model>();
442 case RuntimeType::OrthographicCamera:
443 return qmlElementName<RuntimeType::OrthographicCamera>();
444 case RuntimeType::PerspectiveCamera:
445 return qmlElementName<RuntimeType::PerspectiveCamera>();
446 case RuntimeType::DirectionalLight:
447 return qmlElementName<RuntimeType::DirectionalLight>();
448 case RuntimeType::PointLight:
449 return qmlElementName<RuntimeType::PointLight>();
450 case RuntimeType::SpotLight:
451 return qmlElementName<RuntimeType::SpotLight>();
452 case RuntimeType::Skeleton:
453 return qmlElementName<RuntimeType::Skeleton>();
454 case RuntimeType::Joint:
455 return qmlElementName<RuntimeType::Joint>();
456 case RuntimeType::Skin:
457 return qmlElementName<RuntimeType::Skin>();
458 case RuntimeType::MorphTarget:
459 return qmlElementName<RuntimeType::MorphTarget>();
460 default:
461 return "UNKNOWN_TYPE";
462 }
463}
464
465enum QMLBasicType
466{
467 Bool,
468 Double,
469 Int,
470 List,
471 Real,
472 String,
473 Url,
474 Var,
475 Color,
476 Date,
477 Font,
478 Mat44,
479 Point,
480 Quaternion,
481 Rect,
482 Size,
483 Vector2D,
484 Vector3D,
485 Vector4D,
486 Unknown_Count
487};
488
489static constexpr QByteArrayView qml_basic_types[] {
490 "bool",
491 "double",
492 "int",
493 "list",
494 "real",
495 "string",
496 "url",
497 "var",
498 "color",
499 "date",
500 "font",
501 "matrix4x4",
502 "point",
503 "quaternion",
504 "rect",
505 "size",
506 "vector2d",
507 "vector3d",
508 "vector4d"
509};
510
511static_assert(std::size(qml_basic_types) == QMLBasicType::Unknown_Count, "Missing type?");
512
513static QByteArrayView typeName(QMetaType mt)
514{
515 switch (mt.id()) {
516 case QMetaType::Bool:
517 return qml_basic_types[QMLBasicType::Bool];
518 case QMetaType::Char:
519 case QMetaType::SChar:
520 case QMetaType::UChar:
521 case QMetaType::Char16:
522 case QMetaType::Char32:
523 case QMetaType::QChar:
524 case QMetaType::Short:
525 case QMetaType::UShort:
526 case QMetaType::Int:
527 case QMetaType::UInt:
528 case QMetaType::Long:
529 case QMetaType::ULong:
530 case QMetaType::LongLong:
531 case QMetaType::ULongLong:
532 return qml_basic_types[QMLBasicType::Int];
533 case QMetaType::Float:
534 case QMetaType::Double:
535 return qml_basic_types[QMLBasicType::Real];
536 case QMetaType::QByteArray:
537 case QMetaType::QString:
538 return qml_basic_types[QMLBasicType::String];
539 case QMetaType::QDate:
540 case QMetaType::QTime:
541 case QMetaType::QDateTime:
542 return qml_basic_types[QMLBasicType::Date];
543 case QMetaType::QUrl:
544 return qml_basic_types[QMLBasicType::Url];
545 case QMetaType::QRect:
546 case QMetaType::QRectF:
547 return qml_basic_types[QMLBasicType::Rect];
548 case QMetaType::QSize:
549 case QMetaType::QSizeF:
550 return qml_basic_types[QMLBasicType::Size];
551 case QMetaType::QPoint:
552 case QMetaType::QPointF:
553 return qml_basic_types[QMLBasicType::Point];
554 case QMetaType::QVariant:
555 return qml_basic_types[QMLBasicType::Var];
556 case QMetaType::QColor:
557 return qml_basic_types[QMLBasicType::Color];
558 case QMetaType::QMatrix4x4:
559 return qml_basic_types[QMLBasicType::Mat44];
560 case QMetaType::QVector2D:
561 return qml_basic_types[QMLBasicType::Vector2D];
562 case QMetaType::QVector3D:
563 return qml_basic_types[QMLBasicType::Vector3D];
564 case QMetaType::QVector4D:
565 return qml_basic_types[QMLBasicType::Vector4D];
566 case QMetaType::QQuaternion:
567 return qml_basic_types[QMLBasicType::Quaternion];
568 case QMetaType::QFont:
569 return qml_basic_types[QMLBasicType::Font];
570 default:
571 return qml_basic_types[QMLBasicType::Var];
572 }
573}
574
575using NodeNameMap = QHash<const QSSGSceneDesc::Node *, QString>;
576Q_GLOBAL_STATIC(NodeNameMap, g_nodeNameMap)
577using UniqueIdMap = QHash<QString, const QSSGSceneDesc::Node *>;
578Q_GLOBAL_STATIC(UniqueIdMap, g_idMap)
579// Normally g_idMap will contain all the ids but in some cases
580// (like Animation, not Node) the ids will just be stored
581// to avoid conflict.
582// Now, Animations will be processed after all the Nodes,
583// For Nodes, it is not used.
584using UniqueIdOthers = QSet<QString>;
585Q_GLOBAL_STATIC(UniqueIdOthers, g_idOthers)
586
587static QString getIdForNode(const QSSGSceneDesc::Node &node)
588{
589 static constexpr const char *typeNames[] = {
590 "", // Transform
591 "_camera",
592 "", // Model
593 "_texture",
594 "_material",
595 "_light",
596 "_mesh",
597 "_skin",
598 "_skeleton",
599 "_joint",
600 "_morphtarget",
601 "_unknown"
602 };
603 constexpr uint nameCount = sizeof(typeNames)/sizeof(const char*);
604 const bool nodeHasName = (node.name.size() > 0);
605 uint nameIdx = qMin(a: uint(node.nodeType), b: nameCount);
606 QString name = nodeHasName ? QString::fromUtf8(ba: node.name + typeNames[nameIdx]) : QString::fromLatin1(ba: getQmlElementName(node));
607 QString sanitizedName = QSSGQmlUtilities::sanitizeQmlId(id: name);
608
609 // Make sure we return a unique id.
610 if (const auto it = g_nodeNameMap->constFind(key: &node); it != g_nodeNameMap->constEnd())
611 return *it;
612
613 quint64 id = node.id;
614 int attempts = 1000;
615 QString candidate = sanitizedName;
616 do {
617 if (const auto it = g_idMap->constFind(key: candidate); it == g_idMap->constEnd()) {
618 g_idMap->insert(key: candidate, value: &node);
619 g_nodeNameMap->insert(key: &node, value: candidate);
620 return candidate;
621 }
622
623 candidate = QStringLiteral("%1%2").arg(a: sanitizedName).arg(a: id++);
624 } while (--attempts);
625
626 return candidate;
627}
628
629static QString getIdForAnimation(const QByteArray &inName)
630{
631 QString name = !inName.isEmpty() ? QString::fromUtf8(ba: inName + "_timeline") : "timeline0"_L1;
632 QString sanitizedName = QSSGQmlUtilities::sanitizeQmlId(id: name);
633
634 int attempts = 1000;
635 quint16 id = 0;
636 QString candidate = sanitizedName;
637 do {
638 if (const auto it = g_idMap->constFind(key: candidate); it == g_idMap->constEnd()) {
639 if (const auto oIt = g_idOthers->constFind(value: candidate); oIt == g_idOthers->constEnd()) {
640 g_idOthers->insert(value: candidate);
641 return candidate;
642 }
643 }
644
645 candidate = QStringLiteral("%1%2").arg(a: sanitizedName).arg(a: ++id);
646 } while (--attempts);
647
648 return candidate;
649}
650
651QString stripParentDirectory(const QString &filePath) {
652 QString sourceCopy = filePath;
653 while (sourceCopy.startsWith(c: QChar::fromLatin1(c: '.')) || sourceCopy.startsWith(c: QChar::fromLatin1(c: '/')) || sourceCopy.startsWith(c: QChar::fromLatin1(c: '\\')))
654 sourceCopy.remove(i: 0, len: 1);
655 return sourceCopy;
656}
657
658static const char *blockBegin() { return " {\n"; }
659static const char *blockEnd() { return "}\n"; }
660static const char *comment() { return "// "; }
661static const char *indent() { return " "; }
662
663struct QSSGQmlScopedIndent
664{
665 enum : quint8 { QSSG_INDENT = 4 };
666 explicit QSSGQmlScopedIndent(OutputContext &out) : output(out) { out.indent += QSSG_INDENT; };
667 ~QSSGQmlScopedIndent() { output.indent = qMax(a: output.indent - QSSG_INDENT, b: 0); }
668 OutputContext &output;
669};
670
671static QString indentString(OutputContext &output)
672{
673 QString str;
674 for (quint8 i = 0; i < output.indent; i += QSSGQmlScopedIndent::QSSG_INDENT)
675 str += QString::fromLatin1(ba: indent());
676 return str;
677}
678
679static QTextStream &indent(OutputContext &output)
680{
681 for (quint8 i = 0; i < output.indent; i += QSSGQmlScopedIndent::QSSG_INDENT)
682 output.stream << indent();
683 return output.stream;
684}
685
686static const char *blockBegin(OutputContext &output)
687{
688 ++output.scopeDepth;
689 return blockBegin();
690}
691
692static const char *blockEnd(OutputContext &output)
693{
694 output.scopeDepth = qMax(a: 0, b: output.scopeDepth - 1);
695 return blockEnd();
696}
697
698static void writeImportHeader(OutputContext &output, bool hasAnimation = false)
699{
700 output.stream << "import QtQuick\n"
701 << "import QtQuick3D\n\n";
702 if (hasAnimation)
703 output.stream << "import QtQuick.Timeline\n\n";
704}
705
706static QString toQuotedString(const QString &text) { return QStringLiteral("\"%1\"").arg(a: text); }
707
708static inline QString getMeshFolder() { return QStringLiteral("meshes/"); }
709static inline QString getMeshExtension() { return QStringLiteral(".mesh"); }
710
711QString getMeshSourceName(const QString &name)
712{
713 const auto meshFolder = getMeshFolder();
714 const auto extension = getMeshExtension();
715
716 return QString(meshFolder + name + extension);
717}
718
719static inline QString getTextureFolder() { return QStringLiteral("maps/"); }
720
721static inline QString getAnimationFolder() { return QStringLiteral("animations/"); }
722static inline QString getAnimationExtension() { return QStringLiteral(".qad"); }
723QString getAnimationSourceName(const QString &id, const QString &property, qsizetype index)
724{
725 const auto animationFolder = getAnimationFolder();
726 const auto extension = getAnimationExtension();
727 return QString(animationFolder + id + QStringLiteral("_")
728 + property + QStringLiteral("_")
729 + QString::number(index) + extension);
730}
731
732QString asString(const QVariant &var)
733{
734 return var.toString();
735}
736
737QString builtinQmlType(const QVariant &var)
738{
739 switch (var.metaType().id()) {
740 case QMetaType::QVector2D: {
741 const auto vec2 = qvariant_cast<QVector2D>(v: var);
742 return QLatin1String("Qt.vector2d(") + QString::number(vec2.x()) + QLatin1String(", ") + QString::number(vec2.y()) + QLatin1Char(')');
743 }
744 case QMetaType::QVector3D: {
745 const auto vec3 = qvariant_cast<QVector3D>(v: var);
746 return QLatin1String("Qt.vector3d(") + QString::number(vec3.x()) + QLatin1String(", ")
747 + QString::number(vec3.y()) + QLatin1String(", ")
748 + QString::number(vec3.z()) + QLatin1Char(')');
749 }
750 case QMetaType::QVector4D: {
751 const auto vec4 = qvariant_cast<QVector4D>(v: var);
752 return QLatin1String("Qt.vector4d(") + QString::number(vec4.x()) + QLatin1String(", ")
753 + QString::number(vec4.y()) + QLatin1String(", ")
754 + QString::number(vec4.z()) + QLatin1String(", ")
755 + QString::number(vec4.w()) + QLatin1Char(')');
756 }
757 case QMetaType::QColor: {
758 const auto color = qvariant_cast<QColor>(v: var);
759 return colorToQml(color);
760 }
761 case QMetaType::QQuaternion: {
762 const auto &quat = qvariant_cast<QQuaternion>(v: var);
763 return QLatin1String("Qt.quaternion(") + QString::number(quat.scalar()) + QLatin1String(", ")
764 + QString::number(quat.x()) + QLatin1String(", ")
765 + QString::number(quat.y()) + QLatin1String(", ")
766 + QString::number(quat.z()) + QLatin1Char(')');
767 }
768 case QMetaType::QMatrix4x4: {
769 const auto mat44 = qvariant_cast<QMatrix4x4>(v: var);
770 return QLatin1String("Qt.matrix4x4(")
771 + QString::number(mat44(0, 0)) + u", " + QString::number(mat44(0, 1)) + u", " + QString::number(mat44(0, 2)) + u", " + QString::number(mat44(0, 3)) + u", "
772 + QString::number(mat44(1, 0)) + u", " + QString::number(mat44(1, 1)) + u", " + QString::number(mat44(1, 2)) + u", " + QString::number(mat44(1, 3)) + u", "
773 + QString::number(mat44(2, 0)) + u", " + QString::number(mat44(2, 1)) + u", " + QString::number(mat44(2, 2)) + u", " + QString::number(mat44(2, 3)) + u", "
774 + QString::number(mat44(3, 0)) + u", " + QString::number(mat44(3, 1)) + u", " + QString::number(mat44(3, 2)) + u", " + QString::number(mat44(3, 3)) + u')';
775 }
776 case QMetaType::Float:
777 case QMetaType::Double:
778 case QMetaType::Int:
779 case QMetaType::Char:
780 case QMetaType::Long:
781 case QMetaType::LongLong:
782 case QMetaType::ULong:
783 case QMetaType::ULongLong:
784 case QMetaType::Bool:
785 return var.toString();
786 case QMetaType::QUrl: // QUrl needs special handling. Return empty string to trigger that.
787 default:
788 break;
789 }
790
791 return QString();
792}
793
794QString asString(QSSGSceneDesc::Animation::Channel::TargetProperty prop)
795{
796 if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Position)
797 return QStringLiteral("position");
798 if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Rotation)
799 return QStringLiteral("rotation");
800 if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Scale)
801 return QStringLiteral("scale");
802 if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Weight)
803 return QStringLiteral("weight");
804
805 return QStringLiteral("unknown");
806}
807
808static std::pair<QString, QString> meshAssetName(const QSSGSceneDesc::Scene &scene, const QSSGSceneDesc::Mesh &meshNode, const QDir &outdir)
809{
810 // Returns {name, notValidReason}
811
812 const auto meshFolder = getMeshFolder();
813 const auto meshId = QSSGQmlUtilities::getIdForNode(node: meshNode);
814 const auto meshSourceName = QSSGQmlUtilities::getMeshSourceName(name: meshId);
815 Q_ASSERT(scene.meshStorage.size() > meshNode.idx);
816 const auto &mesh = scene.meshStorage.at(i: meshNode.idx);
817
818 // If a mesh folder does not exist, then create one
819 if (!outdir.exists(name: meshFolder) && !outdir.mkdir(dirName: meshFolder)) {
820 qDebug() << "Failed to create meshes folder at" << outdir;
821 return {}; // Error out
822 }
823
824 const QString path = outdir.path() + QDir::separator() + meshSourceName;
825 QFile file(path);
826 if (!file.open(flags: QIODevice::WriteOnly)) {
827 return {QString(), QStringLiteral("Failed to find mesh at ") + path};
828 }
829
830 if (mesh.save(device: &file) == 0) {
831 return {};
832 }
833
834 return {meshSourceName, QString()};
835};
836
837static std::pair<QString, QString> copyTextureAsset(const QUrl &texturePath, OutputContext &output)
838{
839 // Returns {path, notValidReason}
840
841 // TODO: Use QUrl::resolved() instead of manual string manipulation
842 QString assetPath = output.outdir.isAbsolutePath(path: texturePath.path()) ? texturePath.toString() : texturePath.path();
843 QFileInfo fi(assetPath);
844 if (fi.isRelative() && !output.sourceDir.isEmpty()) {
845 fi = QFileInfo(output.sourceDir + QChar(u'/') + assetPath);
846 }
847 if (!fi.exists()) {
848 indent(output) << comment() << "Source texture path expected: " << getTextureFolder() + texturePath.fileName() << "\n";
849 return {QString(), QStringLiteral("Failed to find texture at ") + assetPath};
850 }
851
852 const auto mapsFolder = getTextureFolder();
853 // If a maps folder does not exist, then create one
854 if (!output.outdir.exists(name: mapsFolder) && !output.outdir.mkdir(dirName: mapsFolder)) {
855 qDebug() << "Failed to create maps folder at" << output.outdir;
856 return {}; // Error out
857 }
858
859 const QString relpath = mapsFolder + fi.fileName();
860 const auto newfilepath = QString(output.outdir.canonicalPath() + QDir::separator() + relpath);
861 if (!QFile::exists(fileName: newfilepath) && !QFile::copy(fileName: fi.canonicalFilePath(), newName: newfilepath)) {
862 qDebug() << "Failed to copy file from" << fi.canonicalFilePath() << "to" << newfilepath;
863 return {};
864 }
865
866 return {relpath, QString()};
867};
868
869static QStringList expandComponents(const QString &value, QMetaType mt)
870{
871 static const QRegularExpression re(QLatin1String("^Qt.[a-z0-9]*\\(([0-9.e\\+\\-, ]*)\\)"));
872 Q_ASSERT(re.isValid());
873
874 switch (mt.id()) {
875 case QMetaType::QVector2D: {
876 QRegularExpressionMatch match = re.match(subject: value);
877 if (match.hasMatch()) {
878 const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(','));
879 if (comp.size() == 2) {
880 return { QLatin1String(".x: ") + comp.at(i: 0).trimmed(),
881 QLatin1String(".y: ") + comp.at(i: 1).trimmed() };
882 }
883 }
884 break;
885 }
886 case QMetaType::QVector3D: {
887 QRegularExpressionMatch match = re.match(subject: value);
888 if (match.hasMatch()) {
889 const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(','));
890 if (comp.size() == 3) {
891 return { QLatin1String(".x: ") + comp.at(i: 0).trimmed(),
892 QLatin1String(".y: ") + comp.at(i: 1).trimmed(),
893 QLatin1String(".z: ") + comp.at(i: 2).trimmed() };
894 }
895 }
896 break;
897 }
898 case QMetaType::QVector4D: {
899 QRegularExpressionMatch match = re.match(subject: value);
900 if (match.hasMatch()) {
901 const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(','));
902 if (comp.size() == 4) {
903 return { QLatin1String(".x: ") + comp.at(i: 0).trimmed(),
904 QLatin1String(".y: ") + comp.at(i: 1).trimmed(),
905 QLatin1String(".z: ") + comp.at(i: 2).trimmed(),
906 QLatin1String(".w: ") + comp.at(i: 3).trimmed() };
907 }
908 }
909 break;
910 }
911 case QMetaType::QQuaternion: {
912 QRegularExpressionMatch match = re.match(subject: value);
913 if (match.hasMatch()) {
914 const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(','));
915 if (comp.size() == 4) {
916 return { QLatin1String(".x: ") + comp.at(i: 0).trimmed(),
917 QLatin1String(".y: ") + comp.at(i: 1).trimmed(),
918 QLatin1String(".z: ") + comp.at(i: 2).trimmed(),
919 QLatin1String(".scalar: ") + comp.at(i: 3).trimmed() };
920 }
921 }
922 break;
923 }
924 default:
925 break;
926 }
927
928 return { value };
929}
930
931static QStringList expandComponentsPartially(const QString &value, QMetaType mt)
932{
933 // Workaround for DS
934 if (mt.id() != QMetaType::QQuaternion)
935 return expandComponents(value, mt);
936
937 return { value };
938}
939
940struct ValueToQmlResult {
941 bool ok = false;
942 QString name;
943 QString value;
944 QString notValidReason;
945 bool isDynamicProperty = false;
946 QStringList expandedProperties;
947};
948
949static ValueToQmlResult valueToQml(const QSSGSceneDesc::Node &target, const QSSGSceneDesc::Property &property, OutputContext &output)
950{
951 ValueToQmlResult result;
952 if (property.value.isNull()) {
953 result.ok = false;
954 result.notValidReason = QStringLiteral("Property value is null");
955 return result;
956 }
957
958 const QVariant &value = property.value;
959 result.name = QString::fromUtf8(ba: property.name);
960 result.isDynamicProperty = property.type == QSSGSceneDesc::Property::Type::Dynamic;
961
962 // Built-in types
963 QString valueAsString = builtinQmlType(var: value);
964 if (valueAsString.size() > 0) {
965 result.value = valueAsString;
966 result.ok = true;
967 } else if (value.metaType().flags() & (QMetaType::IsEnumeration | QMetaType::IsUnsignedEnumeration)) {
968 static const auto qmlEnumString = [](const QLatin1String &element, const QString &enumString) {
969 return QStringLiteral("%1.%2").arg(a: element).arg(a: enumString);
970 };
971 QLatin1String qmlElementName(getQmlElementName(node: target));
972 QString enumValue = asString(var: value);
973 if (enumValue.size() > 0) {
974 result.value = qmlEnumString(qmlElementName, enumValue);
975 result.ok = true;
976 }
977 } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::Flag>()) {
978 QByteArray element(getQmlElementName(node: target));
979 if (element.size() > 0) {
980 const auto flag = qvariant_cast<QSSGSceneDesc::Flag>(v: value);
981 QByteArray keysString = flag.me.valueToKeys(value: int(flag.value));
982 if (keysString.size() > 0) {
983 keysString.prepend(a: element + '.');
984 QByteArray replacement(" | " + element + '.');
985 keysString.replace(before: '|', after: replacement);
986 result.value = QString::fromLatin1(ba: keysString);
987 result.ok = true;
988 }
989 }
990 } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::NodeList *>()) {
991 const auto *list = qvariant_cast<QSSGSceneDesc::NodeList *>(v: value);
992 if (list->count > 0) {
993 const QString indentStr = indentString(output);
994 QSSGQmlScopedIndent scopedIndent(output);
995 const QString listIndentStr = indentString(output);
996
997 QString str;
998 str.append(v: u"[\n");
999
1000 for (int i = 0, end = list->count; i != end; ++i) {
1001 if (i != 0)
1002 str.append(v: u",\n");
1003 str.append(s: listIndentStr);
1004 str.append(s: getIdForNode(node: *(list->head[i])));
1005 }
1006
1007 str.append(s: u'\n' + indentStr + u']');
1008
1009 result.value = str;
1010 result.ok = true;
1011 }
1012 } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::ListView *>()) {
1013 const auto &list = *qvariant_cast<QSSGSceneDesc::ListView *>(v: value);
1014 if (list.count > 0) {
1015 const QString indentStr = indentString(output);
1016 QSSGQmlScopedIndent scopedIndent(output);
1017 const QString listIndentStr = indentString(output);
1018
1019 QString str;
1020 str.append(v: u"[\n");
1021
1022 char *vptr = reinterpret_cast<char *>(list.data);
1023 auto size = list.mt.sizeOf();
1024
1025 for (int i = 0, end = list.count; i != end; ++i) {
1026 if (i != 0)
1027 str.append(v: u",\n");
1028
1029 const QVariant var{list.mt, reinterpret_cast<void *>(vptr + (size * i))};
1030 QString valueString = builtinQmlType(var);
1031 if (valueString.isEmpty())
1032 valueString = asString(var);
1033
1034 str.append(s: listIndentStr);
1035 str.append(s: valueString);
1036 }
1037
1038 str.append(s: u'\n' + indentStr + u']');
1039
1040 result.value = str;
1041 result.ok = true;
1042 }
1043 } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::Node *>()) {
1044 if (const auto node = qvariant_cast<QSSGSceneDesc::Node *>(v: value)) {
1045 // If this assert is triggerd it likely means that the node never got added
1046 // to the scene tree (see: addNode()) or that it's a type not handled as a resource, see:
1047 // writeQmlForResources()
1048 Q_ASSERT(node->id != 0);
1049 // The 'TextureData' node will have its data written out and become
1050 // a source url.
1051
1052 if (node->runtimeType == QSSGSceneDesc::Node::RuntimeType::TextureData) {
1053 result.name = QStringLiteral("source");
1054 result.value = getIdForNode(node: *node->scene->root) + QLatin1Char('.') + getIdForNode(node: *node);
1055 } else {
1056 result.value = getIdForNode(node: *node);
1057 }
1058 result.ok = true;
1059 }
1060 } else if (value.metaType() == QMetaType::fromType<QSSGSceneDesc::Mesh *>()) {
1061 if (const auto meshNode = qvariant_cast<const QSSGSceneDesc::Mesh *>(v: value)) {
1062 Q_ASSERT(meshNode->nodeType == QSSGSceneDesc::Node::Type::Mesh);
1063 Q_ASSERT(meshNode->scene);
1064 const auto &scene = *meshNode->scene;
1065 const auto& [meshSourceName, notValidReason] = meshAssetName(scene, meshNode: *meshNode, outdir: output.outdir);
1066 result.notValidReason = notValidReason;
1067 if (!meshSourceName.isEmpty()) {
1068 result.value = toQuotedString(text: meshSourceName);
1069 result.ok = true;
1070 }
1071 }
1072 } else if (value.metaType() == QMetaType::fromType<QUrl>()) {
1073 if (const auto url = qvariant_cast<QUrl>(v: value); !url.isEmpty()) {
1074 // We need to adjust source url(s) as those should contain the canonical path
1075 QString path;
1076 if (QSSGRenderGraphObject::isTexture(type: target.runtimeType)) {
1077 const auto& [relpath, notValidReason] = copyTextureAsset(texturePath: url, output);
1078 result.notValidReason = notValidReason;
1079 if (!relpath.isEmpty()) {
1080 path = relpath;
1081 }
1082 } else
1083 path = url.path();
1084
1085 if (!path.isEmpty()) {
1086 result.value = toQuotedString(text: path);
1087 result.ok = true;
1088 }
1089 }
1090 } else if (target.runtimeType == QSSGSceneDesc::Material::RuntimeType::CustomMaterial) {
1091 // Workaround the TextureInput item that wraps textures for the Custom material.
1092 if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::Texture *>()) {
1093 if (const auto texture = qvariant_cast<QSSGSceneDesc::Texture *>(v: value)) {
1094 Q_ASSERT(QSSGRenderGraphObject::isTexture(texture->runtimeType));
1095 result.value = QLatin1String("TextureInput { texture: ") +
1096 getIdForNode(node: *texture) + QLatin1String(" }");
1097 result.ok = true;
1098 }
1099 }
1100 } else if (value.metaType() == QMetaType::fromType<QString>()) {
1101 // Plain strings in the scenedesc should map to QML string values
1102 result.value = toQuotedString(text: value.toString());
1103 result.ok = true;
1104 } else {
1105 result.notValidReason = QStringLiteral("Unsupported value type: ") + QString::fromUtf8(utf8: value.metaType().name());
1106 qWarning() << result.notValidReason;
1107 result.ok = false;
1108 }
1109
1110 if (result.ok && (output.options & OutputContext::Options::ExpandValueComponents)) {
1111 result.expandedProperties = ((output.options & OutputContext::Options::DesignStudioWorkarounds) == OutputContext::Options::DesignStudioWorkarounds)
1112 ? expandComponentsPartially(value: result.value, mt: value.metaType())
1113 : expandComponents(value: result.value, mt: value.metaType());
1114 }
1115
1116 return result;
1117}
1118
1119static void writeNodeProperties(const QSSGSceneDesc::Node &node, OutputContext &output)
1120{
1121 QSSGQmlScopedIndent scopedIndent(output);
1122
1123 indent(output) << u"id: "_s << getIdForNode(node) << u'\n';
1124
1125 // Set Object Name if one exists
1126 if (node.name.size()) {
1127 const QString objectName = QString::fromLocal8Bit(ba: node.name);
1128 if (!objectName.startsWith(c: u'*'))
1129 indent(output) << u"objectName: \""_s << node.name << u"\"\n"_s;
1130 }
1131
1132 const auto &properties = node.properties;
1133 auto it = properties.begin();
1134 const auto end = properties.end();
1135 for (; it != end; ++it) {
1136 const auto &property = *it;
1137
1138 const ValueToQmlResult result = valueToQml(target: node, property: *property, output);
1139 if (result.ok) {
1140 if (result.isDynamicProperty) {
1141 indent(output) << "property " << typeName(mt: property->value.metaType()).toByteArray() << ' ' << result.name << u": "_s << result.value << u'\n';
1142 } else if (!QSSGQmlUtilities::PropertyMap::instance()->isDefaultValue(type: node.runtimeType, property: property->name, value: property->value)) {
1143 if (result.expandedProperties.size() > 1) {
1144 for (const auto &va : result.expandedProperties)
1145 indent(output) << result.name << va << u'\n';
1146 } else {
1147 indent(output) << result.name << u": "_s << result.value << u'\n';
1148 }
1149 }
1150 } else if (!result.isDynamicProperty) {
1151 QString message = u"Skipped property: "_s + QString::fromUtf8(ba: property->name);
1152 if (!result.notValidReason.isEmpty())
1153 message.append(s: u", reason: "_s + result.notValidReason);
1154 qDebug() << message;
1155 indent(output) << comment() << message + u'\n';
1156 }
1157 }
1158}
1159
1160static void writeQml(const QSSGSceneDesc::Node &transform, OutputContext &output)
1161{
1162 using namespace QSSGSceneDesc;
1163 Q_ASSERT(transform.nodeType == QSSGSceneDesc::Node::Type::Transform && transform.runtimeType == QSSGSceneDesc::Node::RuntimeType::Node);
1164 indent(output) << qmlElementName<QSSGSceneDesc::Node::RuntimeType::Node>() << blockBegin(output);
1165 writeNodeProperties(node: transform, output);
1166}
1167
1168void writeQml(const QSSGSceneDesc::Material &material, OutputContext &output)
1169{
1170 using namespace QSSGSceneDesc;
1171 Q_ASSERT(material.nodeType == QSSGSceneDesc::Model::Type::Material);
1172 if (material.runtimeType == QSSGSceneDesc::Model::RuntimeType::SpecularGlossyMaterial) {
1173 indent(output) << qmlElementName<Material::RuntimeType::SpecularGlossyMaterial>() << blockBegin(output);
1174 } else if (material.runtimeType == Model::RuntimeType::PrincipledMaterial) {
1175 indent(output) << qmlElementName<Material::RuntimeType::PrincipledMaterial>() << blockBegin(output);
1176 } else if (material.runtimeType == Material::RuntimeType::CustomMaterial) {
1177 indent(output) << qmlElementName<Material::RuntimeType::CustomMaterial>() << blockBegin(output);
1178 } else if (material.runtimeType == Material::RuntimeType::SpecularGlossyMaterial) {
1179 indent(output) << qmlElementName<Material::RuntimeType::SpecularGlossyMaterial>() << blockBegin(output);
1180 } else {
1181 Q_UNREACHABLE();
1182 }
1183
1184 writeNodeProperties(node: material, output);
1185}
1186
1187static void writeQml(const QSSGSceneDesc::Model &model, OutputContext &output)
1188{
1189 using namespace QSSGSceneDesc;
1190 Q_ASSERT(model.nodeType == Node::Type::Model);
1191 indent(output) << qmlElementName<QSSGSceneDesc::Node::RuntimeType::Model>() << blockBegin(output);
1192 writeNodeProperties(node: model, output);
1193}
1194
1195static void writeQml(const QSSGSceneDesc::Camera &camera, OutputContext &output)
1196{
1197 using namespace QSSGSceneDesc;
1198 Q_ASSERT(camera.nodeType == Node::Type::Camera);
1199 if (camera.runtimeType == Camera::RuntimeType::PerspectiveCamera)
1200 indent(output) << qmlElementName<Camera::RuntimeType::PerspectiveCamera>() << blockBegin(output);
1201 else if (camera.runtimeType == Camera::RuntimeType::OrthographicCamera)
1202 indent(output) << qmlElementName<Camera::RuntimeType::OrthographicCamera>() << blockBegin(output);
1203 else
1204 Q_UNREACHABLE();
1205 writeNodeProperties(node: camera, output);
1206}
1207
1208static void writeQml(const QSSGSceneDesc::Texture &texture, OutputContext &output)
1209{
1210 using namespace QSSGSceneDesc;
1211 Q_ASSERT(texture.nodeType == Node::Type::Texture && QSSGRenderGraphObject::isTexture(texture.runtimeType));
1212 if (texture.runtimeType == Texture::RuntimeType::Image2D)
1213 indent(output) << qmlElementName<Texture::RuntimeType::Image2D>() << blockBegin(output);
1214 else if (texture.runtimeType == Texture::RuntimeType::ImageCube)
1215 indent(output) << qmlElementName<Texture::RuntimeType::ImageCube>() << blockBegin(output);
1216 writeNodeProperties(node: texture, output);
1217}
1218
1219static void writeQml(const QSSGSceneDesc::Skin &skin, OutputContext &output)
1220{
1221 using namespace QSSGSceneDesc;
1222 Q_ASSERT(skin.nodeType == Node::Type::Skin && skin.runtimeType == Node::RuntimeType::Skin);
1223 indent(output) << qmlElementName<Node::RuntimeType::Skin>() << blockBegin(output);
1224 writeNodeProperties(node: skin, output);
1225}
1226
1227static void writeQml(const QSSGSceneDesc::MorphTarget &morphTarget, OutputContext &output)
1228{
1229 using namespace QSSGSceneDesc;
1230 Q_ASSERT(morphTarget.nodeType == Node::Type::MorphTarget);
1231 indent(output) << qmlElementName<QSSGSceneDesc::Node::RuntimeType::MorphTarget>() << blockBegin(output);
1232 writeNodeProperties(node: morphTarget, output);
1233}
1234
1235QString getTextureSourceName(const QString &name, const QString &fmt)
1236{
1237 const auto textureFolder = getTextureFolder();
1238
1239 const auto sanitizedName = QSSGQmlUtilities::sanitizeQmlId(id: name);
1240 const auto ext = (fmt.length() != 3) ? u".png"_s
1241 : u"."_s + fmt;
1242
1243 return QString(textureFolder + sanitizedName + ext);
1244}
1245
1246static QString outputTextureAsset(const QSSGSceneDesc::TextureData &textureData, const QDir &outdir)
1247{
1248 if (textureData.data.isEmpty())
1249 return QString();
1250
1251 const auto mapsFolder = getTextureFolder();
1252 const auto id = getIdForNode(node: textureData);
1253 const QString textureSourceName = getTextureSourceName(name: id, fmt: QString::fromUtf8(ba: textureData.fmt));
1254
1255 const bool isCompressed = ((textureData.flgs & quint8(QSSGSceneDesc::TextureData::Flags::Compressed)) != 0);
1256
1257 // If a maps folder does not exist, then create one
1258 if (!outdir.exists(name: mapsFolder) && !outdir.mkdir(dirName: mapsFolder))
1259 return QString(); // Error out
1260
1261 const auto imagePath = QString(outdir.path() + QDir::separator() + textureSourceName);
1262
1263 if (isCompressed) {
1264 QFile file(imagePath);
1265 file.open(flags: QIODevice::WriteOnly);
1266 file.write(data: textureData.data);
1267 file.close();
1268 } else {
1269 const auto &texData = textureData.data;
1270 const auto &size = textureData.sz;
1271 QImage image;
1272 image = QImage(reinterpret_cast<const uchar *>(texData.data()), size.width(), size.height(), QImage::Format::Format_RGBA8888);
1273 if (!image.save(fileName: imagePath))
1274 return QString();
1275 }
1276
1277 return textureSourceName;
1278}
1279
1280static void writeQml(const QSSGSceneDesc::TextureData &textureData, OutputContext &output)
1281{
1282 using namespace QSSGSceneDesc;
1283 Q_ASSERT(textureData.nodeType == Node::Type::Texture && textureData.runtimeType == Node::RuntimeType::TextureData);
1284
1285 QString textureSourcePath = outputTextureAsset(textureData, outdir: output.outdir);
1286
1287 static const auto writeProperty = [](const QString &type, const QString &name, const QString &value) {
1288 return QString::fromLatin1(ba: "property %1 %2: %3").arg(args: type, args: name, args: value);
1289 };
1290
1291 if (!textureSourcePath.isEmpty()) {
1292 const auto type = QLatin1String("url");
1293 const auto name = getIdForNode(node: textureData);
1294
1295 indent(output) << writeProperty(type, name, toQuotedString(text: textureSourcePath)) << '\n';
1296 }
1297}
1298
1299static void writeQml(const QSSGSceneDesc::Light &light, OutputContext &output)
1300{
1301 using namespace QSSGSceneDesc;
1302 Q_ASSERT(light.nodeType == Node::Type::Light);
1303 if (light.runtimeType == Light::RuntimeType::DirectionalLight)
1304 indent(output) << qmlElementName<Light::RuntimeType::DirectionalLight>() << blockBegin(output);
1305 else if (light.runtimeType == Light::RuntimeType::SpotLight)
1306 indent(output) << qmlElementName<Light::RuntimeType::SpotLight>() << blockBegin(output);
1307 else if (light.runtimeType == Light::RuntimeType::PointLight)
1308 indent(output) << qmlElementName<Light::RuntimeType::PointLight>() << blockBegin(output);
1309 else
1310 Q_UNREACHABLE();
1311 writeNodeProperties(node: light, output);
1312}
1313
1314static void writeQml(const QSSGSceneDesc::Skeleton &skeleton, OutputContext &output)
1315{
1316 using namespace QSSGSceneDesc;
1317 Q_ASSERT(skeleton.nodeType == Node::Type::Skeleton && skeleton.runtimeType == Node::RuntimeType::Skeleton);
1318 indent(output) << qmlElementName<Node::RuntimeType::Skeleton>() << blockBegin(output);
1319 writeNodeProperties(node: skeleton, output);
1320}
1321
1322static void writeQml(const QSSGSceneDesc::Joint &joint, OutputContext &output)
1323{
1324 using namespace QSSGSceneDesc;
1325 Q_ASSERT(joint.nodeType == Node::Type::Joint && joint.runtimeType == Node::RuntimeType::Joint);
1326 indent(output) << qmlElementName<Node::RuntimeType::Joint>() << blockBegin(output);
1327 writeNodeProperties(node: joint, output);
1328}
1329
1330static void writeQmlForResourceNode(const QSSGSceneDesc::Node &node, OutputContext &output)
1331{
1332 using namespace QSSGSceneDesc;
1333 Q_ASSERT(output.type == OutputContext::Resource);
1334 Q_ASSERT(QSSGRenderGraphObject::isResource(node.runtimeType) || node.nodeType == Node::Type::Mesh || node.nodeType == Node::Type::Skeleton);
1335
1336 const bool processNode = !node.properties.isEmpty() || (output.type == OutputContext::Resource);
1337 if (processNode) {
1338 QSSGQmlScopedIndent scopedIndent(output);
1339 switch (node.nodeType) {
1340 case Node::Type::Skin:
1341 writeQml(skin: static_cast<const Skin &>(node), output);
1342 break;
1343 case Node::Type::MorphTarget:
1344 writeQml(morphTarget: static_cast<const MorphTarget &>(node), output);
1345 break;
1346 case Node::Type::Skeleton:
1347 writeQml(skeleton: static_cast<const Skeleton &>(node), output);
1348 break;
1349 case Node::Type::Texture:
1350 if (node.runtimeType == Node::RuntimeType::Image2D)
1351 writeQml(texture: static_cast<const Texture &>(node), output);
1352 else if (node.runtimeType == Node::RuntimeType::ImageCube)
1353 writeQml(texture: static_cast<const Texture &>(node), output);
1354 else if (node.runtimeType == Node::RuntimeType::TextureData)
1355 writeQml(textureData: static_cast<const TextureData &>(node), output);
1356 else
1357 Q_UNREACHABLE();
1358 break;
1359 case Node::Type::Material:
1360 writeQml(material: static_cast<const Material &>(node), output);
1361 break;
1362 case Node::Type::Mesh:
1363 // Only handled as a property (see: valueToQml())
1364 break;
1365 default:
1366 qWarning(msg: "Unhandled resource type \'%d\'?", int(node.runtimeType));
1367 break;
1368 }
1369 }
1370
1371 // Do something more convenient if this starts expending to more types...
1372 // NOTE: The TextureData type is written out as a url property...
1373 const bool skipBlockEnd = (node.runtimeType == Node::RuntimeType::TextureData || node.nodeType == Node::Type::Mesh);
1374 if (!skipBlockEnd && processNode && output.scopeDepth != 0) {
1375 QSSGQmlScopedIndent scopedIndent(output);
1376 indent(output) << blockEnd(output);
1377 }
1378}
1379
1380static void writeQmlForNode(const QSSGSceneDesc::Node &node, OutputContext &output)
1381{
1382 using namespace QSSGSceneDesc;
1383
1384 const bool processNode = !(node.properties.isEmpty() && node.children.isEmpty())
1385 || (output.type == OutputContext::Resource);
1386 if (processNode) {
1387 QSSGQmlScopedIndent scopedIndent(output);
1388 switch (node.nodeType) {
1389 case Node::Type::Skeleton:
1390 writeQml(skeleton: static_cast<const Skeleton &>(node), output);
1391 break;
1392 case Node::Type::Joint:
1393 writeQml(joint: static_cast<const Joint &>(node), output);
1394 break;
1395 case Node::Type::Light:
1396 writeQml(light: static_cast<const Light &>(node), output);
1397 break;
1398 case Node::Type::Transform:
1399 writeQml(transform: node, output);
1400 break;
1401 case Node::Type::Camera:
1402 writeQml(camera: static_cast<const Camera &>(node), output);
1403 break;
1404 case Node::Type::Model:
1405 writeQml(model: static_cast<const Model &>(node), output);
1406 break;
1407 default:
1408 break;
1409 }
1410 }
1411
1412 for (const auto &cld : node.children) {
1413 if (!QSSGRenderGraphObject::isResource(type: cld->runtimeType) && output.type == OutputContext::NodeTree) {
1414 QSSGQmlScopedIndent scopedIndent(output);
1415 writeQmlForNode(node: *cld, output);
1416 }
1417 }
1418
1419 // Do something more convenient if this starts expending to more types...
1420 // NOTE: The TextureData type is written out as a url property...
1421 const bool skipBlockEnd = (node.runtimeType == Node::RuntimeType::TextureData || node.nodeType == Node::Type::Mesh);
1422 if (!skipBlockEnd && processNode && output.scopeDepth != 0) {
1423 QSSGQmlScopedIndent scopedIndent(output);
1424 indent(output) << blockEnd(output);
1425 }
1426}
1427
1428void writeQmlForResources(const QSSGSceneDesc::Scene::ResourceNodes &resources, OutputContext &output)
1429{
1430 auto sortedResources = resources;
1431 std::sort(first: sortedResources.begin(), last: sortedResources.end(), comp: [](const QSSGSceneDesc::Node *a, const QSSGSceneDesc::Node *b) {
1432 using RType = QSSGSceneDesc::Node::RuntimeType;
1433 if (a->runtimeType == RType::TextureData && b->runtimeType != RType::TextureData)
1434 return true;
1435 if (a->runtimeType == RType::ImageCube && (b->runtimeType != RType::TextureData && b->runtimeType != RType::ImageCube))
1436 return true;
1437 if (a->runtimeType == RType::Image2D && (b->runtimeType != RType::TextureData && b->runtimeType != RType::Image2D))
1438 return true;
1439
1440 return false;
1441 });
1442 for (const auto &res : std::as_const(t&: sortedResources))
1443 writeQmlForResourceNode(node: *res, output);
1444}
1445
1446static void generateKeyframeData(const QSSGSceneDesc::Animation::Channel &channel, QByteArray &keyframeData)
1447{
1448#ifdef QT_QUICK3D_ENABLE_RT_ANIMATIONS
1449 QCborStreamWriter writer(&keyframeData);
1450 // Start root array
1451 writer.startArray();
1452 // header name
1453 writer.append(str: "QTimelineKeyframes");
1454 // file version. Increase this if the format changes.
1455 const int keyframesDataVersion = 1;
1456 writer.append(i: keyframesDataVersion);
1457 writer.append(i: int(channel.keys.at(i: 0)->getValueQMetaType()));
1458
1459 // Start Keyframes array
1460 writer.startArray();
1461 quint8 compEnd = quint8(channel.keys.at(i: 0)->getValueType());
1462 bool isQuaternion = false;
1463 if (compEnd == quint8(QSSGSceneDesc::Animation::KeyPosition::ValueType::Quaternion)) {
1464 isQuaternion = true;
1465 compEnd = 3;
1466 } else {
1467 compEnd++;
1468 }
1469 for (const auto &key : channel.keys) {
1470 writer.append(f: key->time);
1471 // Easing always linear
1472 writer.append(i: QEasingCurve::Linear);
1473 if (isQuaternion)
1474 writer.append(f: key->value[3]);
1475 for (quint8 i = 0; i < compEnd; ++i)
1476 writer.append(f: key->value[i]);
1477 }
1478 // End Keyframes array
1479 writer.endArray();
1480 // End root array
1481 writer.endArray();
1482#else
1483 Q_UNUSED(channel)
1484 Q_UNUSED(keyframeData)
1485#endif // QT_QUICK3D_ENABLE_RT_ANIMATIONS
1486}
1487
1488QPair<QString, QString> writeQmlForAnimation(const QSSGSceneDesc::Animation &anim, qsizetype index, OutputContext &output, bool useBinaryKeyframes = true, bool generateTimelineAnimations = true)
1489{
1490 indent(output) << "Timeline {\n";
1491
1492 QSSGQmlScopedIndent scopedIndent(output);
1493 // The duration property of the TimelineAnimation is an int...
1494 const int duration = qCeil(v: anim.length);
1495 // Use the same name for objectName and id
1496 const QString animationId = getIdForAnimation(inName: anim.name);
1497 indent(output) << "id: " << animationId << "\n";
1498 QString animationName = animationId;
1499 if (!anim.name.isEmpty())
1500 animationName = QString::fromLocal8Bit(ba: anim.name);
1501 indent(output) << "objectName: \"" << animationName << "\"\n";
1502 indent(output) << "property real framesPerSecond: " << anim.framesPerSecond << "\n";
1503 indent(output) << "startFrame: 0\n";
1504 indent(output) << "endFrame: " << duration << "\n";
1505 indent(output) << "currentFrame: 0\n";
1506 // Only generate the TimelineAnimation component here if requested
1507 // enabled is only set to true up front if we expect to autoplay
1508 // the generated TimelineAnimation
1509 if (generateTimelineAnimations) {
1510 indent(output) << "enabled: true\n";
1511 indent(output) << "animations: TimelineAnimation {\n";
1512 {
1513 QSSGQmlScopedIndent scopedIndent(output);
1514 indent(output) << "duration: " << duration << "\n";
1515 indent(output) << "from: 0\n";
1516 indent(output) << "to: " << duration << "\n";
1517 indent(output) << "running: true\n";
1518 indent(output) << "loops: Animation.Infinite\n";
1519 }
1520 indent(output) << blockEnd(output);
1521 }
1522
1523 for (const auto &channel : anim.channels) {
1524 QString id = getIdForNode(node: *channel->target);
1525 QString propertyName = asString(prop: channel->targetProperty);
1526
1527 indent(output) << "KeyframeGroup {\n";
1528 {
1529 QSSGQmlScopedIndent scopedIndent(output);
1530 indent(output) << "target: " << id << "\n";
1531 indent(output) << "property: " << toQuotedString(text: propertyName) << "\n";
1532 if (useBinaryKeyframes && channel->keys.size() != 1) {
1533 const auto animFolder = getAnimationFolder();
1534 const auto animSourceName = getAnimationSourceName(id, property: propertyName, index);
1535 if (!output.outdir.exists(name: animFolder) && !output.outdir.mkdir(dirName: animFolder)) {
1536 // Make a warning
1537 continue;
1538 }
1539 QFile file(output.outdir.path() + QDir::separator() + animSourceName);
1540 if (!file.open(flags: QIODevice::WriteOnly))
1541 continue;
1542 QByteArray keyframeData;
1543 // It is possible to store this keyframeData but we have to consider
1544 // all the cases including runtime only or writeQml only.
1545 // For now, we will generate it for each case.
1546 generateKeyframeData(channel: *channel, keyframeData);
1547 file.write(data: keyframeData);
1548 file.close();
1549 indent(output) << "keyframeSource: " << toQuotedString(text: animSourceName) << "\n";
1550 } else {
1551 Q_ASSERT(!channel->keys.isEmpty());
1552 for (const auto &key : channel->keys) {
1553 indent(output) << "Keyframe {\n";
1554 {
1555 QSSGQmlScopedIndent scopedIndent(output);
1556 indent(output) << "frame: " << key->time << "\n";
1557 indent(output) << "value: " << variantToQml(variant: key->getValue()) << "\n";
1558 }
1559 indent(output) << blockEnd(output);
1560 }
1561 }
1562 }
1563 indent(output) << blockEnd(output);
1564 }
1565 return {animationName, animationId};
1566}
1567
1568void writeQml(const QSSGSceneDesc::Scene &scene, QTextStream &stream, const QDir &outdir, const QJsonObject &optionsObject)
1569{
1570 static const auto checkBooleanOption = [](const QLatin1String &optionName, const QJsonObject &options, bool defaultValue = false) {
1571 const auto it = options.constFind(key: optionName);
1572 const auto end = options.constEnd();
1573 QJsonValue value;
1574 if (it != end) {
1575 if (it->isObject())
1576 value = it->toObject().value(key: QLatin1String("value"));
1577 else
1578 value = it.value();
1579 }
1580 return value.toBool(defaultValue);
1581 };
1582
1583 auto root = scene.root;
1584 Q_ASSERT(root);
1585
1586 QJsonObject options = optionsObject;
1587
1588 if (auto it = options.constFind(key: QLatin1String("options")), end = options.constEnd(); it != end)
1589 options = it->toObject();
1590
1591 quint8 outputOptions{ OutputContext::Options::None };
1592 if (checkBooleanOption(QLatin1String("expandValueComponents"), options))
1593 outputOptions |= OutputContext::Options::ExpandValueComponents;
1594
1595 // Workaround for design studio type components
1596 if (checkBooleanOption(QLatin1String("designStudioWorkarounds"), options))
1597 outputOptions |= OutputContext::Options::DesignStudioWorkarounds;
1598
1599 const bool useBinaryKeyframes = checkBooleanOption("useBinaryKeyframes"_L1, options);
1600 const bool generateTimelineAnimations = !checkBooleanOption("manualAnimations"_L1, options);
1601
1602 OutputContext output { .stream: stream, .outdir: outdir, .sourceDir: scene.sourceDir, .indent: 0, .type: OutputContext::Header, .options: outputOptions };
1603
1604 writeImportHeader(output, hasAnimation: scene.animations.count() > 0);
1605
1606 output.type = OutputContext::RootNode;
1607 writeQml(transform: *root, output); // Block scope will be left open!
1608 stream << "\n";
1609 stream << indent() << "// Resources\n";
1610 output.type = OutputContext::Resource;
1611 writeQmlForResources(resources: scene.resources, output);
1612 output.type = OutputContext::NodeTree;
1613 stream << "\n";
1614 stream << indent() << "// Nodes:\n";
1615 for (const auto &cld : root->children)
1616 writeQmlForNode(node: *cld, output);
1617
1618 // animations
1619 qsizetype animId = 0;
1620 stream << "\n";
1621 stream << indent() << "// Animations:\n";
1622 QList<QPair<QString, QString>> animationMap;
1623 for (const auto &cld : scene.animations) {
1624 QSSGQmlScopedIndent scopedIndent(output);
1625 auto mapValues = writeQmlForAnimation(anim: *cld, index: animId++, output, useBinaryKeyframes, generateTimelineAnimations);
1626 animationMap.append(t: mapValues);
1627 indent(output) << blockEnd(output);
1628 }
1629
1630 if (!generateTimelineAnimations) {
1631 // Expose a map of timelines
1632 stream << "\n";
1633 stream << indent() << "// An exported mapping of Timelines (--manualAnimations)\n";
1634 stream << indent() << "property var timelineMap: {\n";
1635 QSSGQmlScopedIndent scopedIndent(output);
1636 for (const auto &mapValues : animationMap) {
1637 QSSGQmlScopedIndent scopedIndent(output);
1638 indent(output) << "\"" << mapValues.first << "\": " << mapValues.second << ",\n";
1639 }
1640 indent(output) << blockEnd(output);
1641 stream << indent() << "// A simple list of Timelines (--manualAnimations)\n";
1642 stream << indent() << "property var timelineList: [\n";
1643 for (const auto &mapValues : animationMap) {
1644 QSSGQmlScopedIndent scopedIndent(output);
1645 indent(output) << mapValues.second << ",\n";
1646 }
1647 indent(output) << "]\n";
1648 }
1649
1650
1651 // close the root
1652 indent(output) << blockEnd(output);
1653}
1654
1655void createTimelineAnimation(const QSSGSceneDesc::Animation &anim, QObject *parent, bool isEnabled, bool useBinaryKeyframes)
1656{
1657#ifdef QT_QUICK3D_ENABLE_RT_ANIMATIONS
1658 auto timeline = new QQuickTimeline(parent);
1659 auto timelineKeyframeGroup = timeline->keyframeGroups();
1660 for (const auto &channel : anim.channels) {
1661 auto keyframeGroup = new QQuickKeyframeGroup(timeline);
1662 keyframeGroup->setTargetObject(channel->target->obj);
1663 keyframeGroup->setProperty(asString(prop: channel->targetProperty));
1664
1665 Q_ASSERT(!channel->keys.isEmpty());
1666 if (useBinaryKeyframes) {
1667 QByteArray keyframeData;
1668 generateKeyframeData(channel: *channel, keyframeData);
1669
1670 keyframeGroup->setKeyframeData(keyframeData);
1671 } else {
1672 auto keyframes = keyframeGroup->keyframes();
1673 for (const auto &key : channel->keys) {
1674 auto keyframe = new QQuickKeyframe(keyframeGroup);
1675 keyframe->setFrame(key->time);
1676 keyframe->setValue(key->getValue());
1677 keyframes.append(&keyframes, keyframe);
1678 }
1679 }
1680 (qobject_cast<QQmlParserStatus *>(object: keyframeGroup))->componentComplete();
1681 timelineKeyframeGroup.append(&timelineKeyframeGroup, keyframeGroup);
1682 }
1683 timeline->setEndFrame(anim.length);
1684 timeline->setEnabled(isEnabled);
1685
1686 auto timelineAnimation = new QQuickTimelineAnimation(timeline);
1687 timelineAnimation->setObjectName(anim.name);
1688 timelineAnimation->setDuration(int(anim.length));
1689 timelineAnimation->setFrom(0.0f);
1690 timelineAnimation->setTo(anim.length);
1691 timelineAnimation->setLoops(QQuickTimelineAnimation::Infinite);
1692 timelineAnimation->setTargetObject(timeline);
1693
1694 (qobject_cast<QQmlParserStatus *>(object: timeline))->componentComplete();
1695
1696 timelineAnimation->setRunning(true);
1697#else // QT_QUICK3D_ENABLE_RT_ANIMATIONS
1698 Q_UNUSED(anim)
1699 Q_UNUSED(parent)
1700 Q_UNUSED(isEnabled)
1701 Q_UNUSED(useBinaryKeyframes)
1702#endif // QT_QUICK3D_ENABLE_RT_ANIMATIONS
1703}
1704
1705void writeQmlComponent(const QSSGSceneDesc::Node &node, QTextStream &stream, const QDir &outDir)
1706{
1707 using namespace QSSGSceneDesc;
1708
1709 QSSG_ASSERT(node.scene != nullptr, return);
1710
1711 if (node.runtimeType == Material::RuntimeType::CustomMaterial) {
1712 QString sourceDir = node.scene->sourceDir;
1713 OutputContext output { .stream: stream, .outdir: outDir, .sourceDir: sourceDir, .indent: 0, .type: OutputContext::Resource };
1714 writeImportHeader(output);
1715 writeQml(material: static_cast<const Material &>(node), output);
1716 // Resources, if any, are written out as properties on the component
1717 const auto &resources = node.scene->resources;
1718 writeQmlForResources(resources, output);
1719 indent(output) << blockEnd(output);
1720 } else {
1721 Q_UNREACHABLE(); // Only implemented for Custom material at this point.
1722 }
1723}
1724
1725}
1726
1727QT_END_NAMESPACE
1728

source code of qtquick3d/src/assetutils/qssgqmlutilities.cpp