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
gltfsceneconverter.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
7
8#include <QtQuick3DAssetUtils/private/qssgqmlutilities_p.h>
9#include <QtQuick3DGltf/private/qssggltfparser_p.h>
10
11#include <QtQuick3D/private/qquick3dmodel_p.h>
12#include <QtQuick3D/private/qquick3dmorphtarget_p.h>
13#include <QtQuick3D/private/qquick3dnode_p.h>
14#include <QtQuick3D/private/qquick3dorthographiccamera_p.h>
15#include <QtQuick3D/private/qquick3dperspectivecamera_p.h>
16#include <QtQuick3D/private/qquick3dprincipledmaterial_p.h>
17#include <QtQuick3D/private/qquick3dskin_p.h>
18#include <QtQuick3D/private/qquick3dspecularglossymaterial_p.h>
19#include <QtQuick3D/private/qquick3dtexture_p.h>
20
21#include <QtQuick3DGltf/private/qssggltfaccessorreader_p.h>
22#include <QtQuick3DGltf/private/qssggltfresourceresolver_p.h>
23#include <QtQuick3DUtils/private/qssgutils_p.h>
24
25#if QT_CONFIG(concurrent)
26#include <QtConcurrent/qtconcurrentmap.h>
27#endif
28#include <QtCore/qdir.h>
29#include <QtCore/qmath.h>
30#include <QtGui/qimagereader.h>
31
33
34using namespace QSSGGltf;
35
36namespace {
37
38bool checkBooleanOption(QLatin1StringView optionName, const QJsonObject &options, bool defaultValue = false)
39{
40 const auto opt = options.constFind(optionName);
41 if (opt == options.constEnd())
42 return defaultValue;
43 const auto value = opt->toObject().value(QLatin1String("value"));
44 return value.toBool(defaultValue);
45}
46
47float getRealOption(QLatin1StringView optionName, const QJsonObject &options, float defaultValue)
48{
49 const auto opt = options.constFind(optionName);
50 if (opt == options.constEnd())
51 return defaultValue;
52 const auto value = opt->toObject().value(QLatin1String("value"));
53 return float(value.toDouble(defaultValue));
54}
55
56QString getStringOption(QLatin1StringView optionName, const QJsonObject &options)
57{
58 const auto opt = options.constFind(optionName);
59 if (opt == options.constEnd())
60 return {};
61 return opt->toObject().value(QLatin1String("value")).toString();
62}
63
64void decomposeMatrix(const QMatrix4x4 &matrix, QVector3D &translation, QQuaternion &rotation, QVector3D &scale)
65{
66 translation = matrix.column(3).toVector3D();
67
68 QVector3D columns[3] = { matrix.column(0).toVector3D(),
69 matrix.column(1).toVector3D(),
70 matrix.column(2).toVector3D() };
71 scale = QVector3D(columns[0].length(), columns[1].length(), columns[2].length());
72
73 // Fold a mirroring into one scale axis
74 if (matrix.determinant() < 0.0f)
75 scale.setX(-scale.x());
76
77 for (int i = 0; i < 3; ++i) {
78 const float axisScale = i == 0 ? scale.x() : (i == 1 ? scale.y() : scale.z());
79 if (!qFuzzyIsNull(axisScale))
80 columns[i] /= axisScale;
81 }
82
83 const float rotationValues[9] = {
84 columns[0].x(), columns[1].x(), columns[2].x(),
85 columns[0].y(), columns[1].y(), columns[2].y(),
86 columns[0].z(), columns[1].z(), columns[2].z()
87 };
88 rotation = QQuaternion::fromRotationMatrix(QMatrix3x3(rotationValues)).normalized();
89}
90
91// The WebP plugin is in qtimageformats, which is not necessarily deployed
92bool isWebpSupported()
93{
94 static const bool supported = QImageReader::supportedImageFormats().contains(QByteArrayLiteral("webp"));
95 return supported;
96}
97
98} // namespace
99
100GltfSceneConverter::Options GltfSceneConverter::parseOptions(const QJsonObject &optionsObject)
101{
102 // Options may arrive wrapped in an "options" object (balsam) or flat
103 QJsonObject options = optionsObject;
104 if (auto it = options.constFind(QLatin1String("options")); it != options.constEnd())
105 options = it->toObject();
106
107 Options result;
108 result.designStudioWorkarounds = checkBooleanOption(QLatin1StringView("designStudioWorkarounds"), options);
109 result.joinIdenticalVertices = checkBooleanOption(QLatin1StringView("joinIdenticalVertices"), options, true);
110 result.useFloatJointIndices = checkBooleanOption(QLatin1StringView("useFloatJointIndices"), options);
111 result.generateSmoothNormals = checkBooleanOption(QLatin1StringView("generateSmoothNormals"), options);
112 result.forceTangentGeneration = checkBooleanOption(QLatin1StringView("calculateTangentSpace"), options);
113 result.generateMipMaps = checkBooleanOption(QLatin1StringView("generateMipMaps"), options);
114 result.animationSampleRate = getRealOption(QLatin1StringView("animationSampleRate"), options, 30.0f);
115 result.materialVariant = getStringOption(QLatin1StringView("materialVariant"), options);
116
117 result.generateMeshLODs = checkBooleanOption(QLatin1StringView("generateMeshLevelsOfDetail"), options);
118 if (result.generateMeshLODs) {
119 if (checkBooleanOption(QLatin1StringView("recalculateLodNormals"), options)) {
120 const float mergeAngle =
121 getRealOption(QLatin1StringView("recalculateLodNormalsMergeAngle"), options, 60.0f);
122 result.lodNormalMergeAngle = qBound(0.0f, mergeAngle, 270.0f);
123 const float splitAngle =
124 getRealOption(QLatin1StringView("recalculateLodNormalsSplitAngle"), options, 25.0f);
125 result.lodNormalSplitAngle = qBound(0.0f, splitAngle, 270.0f);
126 } else {
127 result.lodNormalMergeAngle = 0.0f;
128 result.lodNormalSplitAngle = 0.0f;
129 }
130 }
131
132 if (checkBooleanOption(QLatin1StringView("globalScale"), options))
133 result.globalScaleValue = getRealOption(QLatin1StringView("globalScaleValue"), options, 1.0f);
134
135 return result;
136}
137
138QString GltfSceneConverter::convert(const QSSGGltfDocument &document, const QJsonObject &optionsObject,
139 const QFileInfo &sourceFile, QSSGSceneDesc::Scene &targetScene)
140{
141 m_document = &document;
142 m_scene = &targetScene;
143 m_options = parseOptions(optionsObject);
144 if (!m_options.materialVariant.isEmpty()) {
145 m_options.materialVariantIndex = int(document.materialVariants.indexOf(m_options.materialVariant));
146 if (m_options.materialVariantIndex < 0) {
147 // Not an error, since one option may be applied to a batch of assets
148 qCWarning(lcQuick3DGltf) << "Asset has no material variant" << m_options.materialVariant
149 << "- available variants:" << document.materialVariants;
150 }
151 }
152 // Baking one variant leaves nothing to switch between
153 if (m_options.materialVariant.isEmpty())
154 targetScene.materialVariants = document.materialVariants;
155 m_nodeMap.clear();
156 m_meshMap.clear();
157 m_materialMap.clear();
158 m_textureMap.clear();
159 m_textureDataMap.clear();
160 m_skinMap.clear();
161 m_meshUsedPrimitives.clear();
162 m_meshMorphInfo.clear();
163 m_morphTargetMap.clear();
164 m_builtMeshes.clear();
165
166 targetScene.sourceDir = sourceFile.path();
167 // For simplicity, and convenience, we'll just use the file path as the id.
168 targetScene.id = sourceFile.canonicalFilePath();
169
170 if (!targetScene.root) {
171 auto root = new QSSGSceneDesc::Node(QByteArrayLiteral("Root"), QSSGSceneDesc::Node::Type::Transform,
172 QSSGSceneDesc::Node::RuntimeType::Node);
173 QSSGSceneDesc::addNode(targetScene, *root);
174 }
175
176 if (!qFuzzyCompare(m_options.globalScaleValue, 1.0f) && !qFuzzyIsNull(m_options.globalScaleValue)) {
177 const float gscale = m_options.globalScaleValue;
178 QSSGSceneDesc::setProperty(*targetScene.root, "scale", &QQuick3DNode::setScale,
179 QVector3D { gscale, gscale, gscale });
180 }
181
182 int sceneIndex = document.scene;
183 if (sceneIndex < 0 && !document.scenes.isEmpty()) {
184 qCWarning(lcQuick3DGltf) << "Document has no default scene, using scene 0";
185 sceneIndex = 0;
186 }
187 if (sceneIndex < 0)
188 return QStringLiteral("Document contains no scene");
189
190 if (document.extensionsRequired.contains(QLatin1String("EXT_texture_webp")) && !isWebpSupported())
191 return QStringLiteral("Asset requires EXT_texture_webp, but no WebP image plugin is available");
192
193 // Mesh building only reads the document, so it runs in parallel
194 {
195 QList<int> meshIndexes(document.meshes.size());
196 std::iota(meshIndexes.begin(), meshIndexes.end(), 0);
197 const auto buildOneMesh = [&](int meshIndex) {
198 BuiltMesh result;
199 result.mesh = GltfMeshBuilder::buildMesh(document, document.meshes.at(meshIndex), m_options,
200 &result.usedPrimitives, &result.morphInfo);
201 return result;
202 };
203#if QT_CONFIG(concurrent)
204 QList<BuiltMesh> built = QtConcurrent::blockingMapped(meshIndexes, buildOneMesh);
205#else
206 QList<BuiltMesh> built;
207 built.reserve(meshIndexes.size());
208 for (const int meshIndex : meshIndexes)
209 built.append(buildOneMesh(meshIndex));
210#endif
211 for (qsizetype meshIndex = 0; meshIndex < built.size(); ++meshIndex)
212 m_builtMeshes.insert(int(meshIndex), std::move(built[meshIndex]));
213 }
214
215 for (const int nodeIndex : document.scenes.at(sceneIndex).nodes)
216 processNode(nodeIndex, *targetScene.root);
217
218 // Skins can only be resolved once all joint nodes exist
219 convertSkins();
220 convertAnimations();
221
222 return QString();
223}
224
225void GltfSceneConverter::convertSkins()
226{
227 for (auto it = m_skinMap.constBegin(); it != m_skinMap.constEnd(); ++it) {
228 const Skin &source = m_document->skins.at(it.key());
229 QSSGSceneDesc::Skin *skinNode = it.value();
230
231 QVarLengthArray<QSSGSceneDesc::Node *> joints;
232 QList<QMatrix4x4> inverseBindPoses;
233 joints.reserve(source.joints.size());
234
235 const QList<float> ibmData = QSSGGltfAccessorReader::readAsFloats(*m_document, source.inverseBindMatrices);
236 const bool hasBindMatrices = ibmData.size() >= source.joints.size() * 16;
237
238 for (qsizetype jointIndex = 0; jointIndex < source.joints.size(); ++jointIndex) {
239 QSSGSceneDesc::Node *jointNode = m_nodeMap.value(source.joints.at(jointIndex), nullptr);
240 if (!jointNode) {
241 qCWarning(lcQuick3DGltf) << "Skin" << it.key() << "joint" << jointIndex
242 << "references a node outside the scene";
243 continue;
244 }
245 joints.push_back(jointNode);
246 if (hasBindMatrices) {
247 // glTF matrices are column-major; QMatrix4x4(float*) is row-major
248 inverseBindPoses.append(QMatrix4x4(ibmData.constData() + jointIndex * 16).transposed());
249 } else {
250 inverseBindPoses.append(QMatrix4x4());
251 }
252 }
253
254 QSSGSceneDesc::setProperty(*skinNode, "joints", &QQuick3DSkin::joints, joints);
255 QSSGSceneDesc::setProperty(*skinNode, "inverseBindPoses", &QQuick3DSkin::setInverseBindPoses, inverseBindPoses);
256 }
257}
258
259void GltfSceneConverter::convertAnimations()
260{
261 using QSSGAnimation = QSSGSceneDesc::Animation;
262
263 const auto makeKey = [](float timeMs, const QVector4D &value, QSSGAnimation::KeyPosition::ValueType valueType) {
264 const auto flag = quint16(quint16(QSSGAnimation::KeyPosition::KeyType::Time) | quint16(valueType));
265 return new QSSGAnimation::KeyPosition { value, timeMs, flag };
266 };
267
268 for (qsizetype animationIndex = 0; animationIndex < m_document->animations.size(); ++animationIndex) {
269 const Animation &sourceAnimation = m_document->animations.at(animationIndex);
270
271 QSSGAnimation targetAnimation;
272 targetAnimation.name = sourceAnimation.name.isEmpty()
273 ? QByteArrayLiteral("animation") + QByteArray::number(animationIndex)
274 : sourceAnimation.name.toUtf8();
275 // Key times are in milliseconds
276 targetAnimation.framesPerSecond = 1000.0f;
277
278 for (const AnimationChannel &channel : sourceAnimation.channels) {
279 QSSGSceneDesc::Node *targetNode = m_nodeMap.value(channel.targetNode, nullptr);
280 if (!targetNode)
281 continue;
282
283 if (channel.path == AnimationChannel::Path::Weights) {
284 // Weights are interleaved per key across all targets
285 const QList<QSSGSceneDesc::MorphTarget *> morphTargets = m_morphTargetMap.value(channel.targetNode);
286 if (morphTargets.isEmpty())
287 continue;
288 const Node &sourceNode = m_document->nodes.at(channel.targetNode);
289 int sourceTargetCount = 0;
290 for (const MeshPrimitive &primitive : m_document->meshes.at(sourceNode.mesh).primitives)
291 sourceTargetCount = qMax(sourceTargetCount, int(primitive.targets.size()));
292 if (sourceTargetCount == 0)
293 continue;
294
295 const AnimationSampler &weightSampler = sourceAnimation.samplers.at(channel.sampler);
296 // CUBICSPLINE stores in-tangent, value, out-tangent per key
297 const bool isCubic = weightSampler.interpolation == AnimationSampler::Interpolation::CubicSpline;
298 if (isCubic)
299 qCWarning(lcQuick3DGltf) << "Cubic spline morph weight animation is not supported, using linear";
300 const bool isStep = weightSampler.interpolation == AnimationSampler::Interpolation::Step;
301 const int valuesPerKey = isCubic ? 3 : 1;
302 const QList<float> times = QSSGGltfAccessorReader::readAsFloats(*m_document, weightSampler.input);
303 const QList<float> values = QSSGGltfAccessorReader::readAsFloats(*m_document, weightSampler.output);
304 const qsizetype keyCount =
305 qMin(qsizetype(times.size()), values.size() / (sourceTargetCount * valuesPerKey));
306
307 for (qsizetype targetIndex = 0; targetIndex < morphTargets.size(); ++targetIndex) {
308 auto *weightChannel = new QSSGAnimation::Channel;
309 weightChannel->target = morphTargets.at(targetIndex);
310 weightChannel->targetProperty = QSSGAnimation::Channel::TargetProperty::Weight;
311 for (qsizetype key = 0; key < keyCount; ++key) {
312 const qsizetype block = key * valuesPerKey + (isCubic ? 1 : 0);
313 const QVector4D weight(values.value(block * sourceTargetCount + targetIndex), 0.0f, 0.0f, 0.0f);
314 weightChannel->keys.push_back(makeKey(times.at(key) * 1000.0f, weight,
315 QSSGAnimation::KeyPosition::ValueType::Number));
316 if (isStep && key + 1 < keyCount) {
317 const float holdTime = times.at(key + 1) * 1000.0f - 1.0f;
318 if (holdTime > times.at(key) * 1000.0f) {
319 weightChannel->keys.push_back(makeKey(holdTime, weight,
320 QSSGAnimation::KeyPosition::ValueType::Number));
321 }
322 }
323 }
324 if (weightChannel->keys.isEmpty()) {
325 delete weightChannel;
326 continue;
327 }
328 targetAnimation.length = qMax(targetAnimation.length, weightChannel->keys.last()->time);
329 targetAnimation.channels.push_back(weightChannel);
330 }
331 continue;
332 }
333
334 const AnimationSampler &sampler = sourceAnimation.samplers.at(channel.sampler);
335 const QList<float> times = QSSGGltfAccessorReader::readAsFloats(*m_document, sampler.input);
336 const QList<float> values = QSSGGltfAccessorReader::readAsFloats(*m_document, sampler.output);
337 if (times.isEmpty())
338 continue;
339
340 const bool isRotation = channel.path == AnimationChannel::Path::Rotation;
341 const int components = isRotation ? 4 : 3;
342 const auto valueType = isRotation ? QSSGAnimation::KeyPosition::ValueType::Quaternion
343 : QSSGAnimation::KeyPosition::ValueType::Vec3;
344
345 // CUBICSPLINE stores in-tangent, value, out-tangent per key
346 const int valuesPerKey = sampler.interpolation == AnimationSampler::Interpolation::CubicSpline ? 3 : 1;
347 const qsizetype keyCount = qMin(qsizetype(times.size()), values.size() / (components * valuesPerKey));
348 if (keyCount == 0)
349 continue;
350
351 const auto valueAt = [&](qsizetype key, int part = 0) {
352 // part: 0 in-tangent, 1 value, 2 out-tangent
353 const qsizetype base = (key * valuesPerKey + (valuesPerKey == 3 ? part : 0)) * components;
354 return QVector4D(values.value(base), values.value(base + 1), values.value(base + 2),
355 components == 4 ? values.value(base + 3) : 0.0f);
356 };
357 // Keep consecutive keys in one hemisphere for shortest-path interpolation
358 QVector4D previousValue;
359 const auto fixQuaternionContinuity = [&](QVector4D value) {
360 if (isRotation && QVector4D::dotProduct(value, previousValue) < 0.0f)
361 value = -value;
362 previousValue = value;
363 return value;
364 };
365
366 auto *targetChannel = new QSSGAnimation::Channel;
367 targetChannel->target = targetNode;
368 targetChannel->targetProperty = channel.path == AnimationChannel::Path::Translation
369 ? QSSGAnimation::Channel::TargetProperty::Position
370 : (isRotation ? QSSGAnimation::Channel::TargetProperty::Rotation
371 : QSSGAnimation::Channel::TargetProperty::Scale);
372
373 switch (sampler.interpolation) {
374 case AnimationSampler::Interpolation::Linear:
375 for (qsizetype key = 0; key < keyCount; ++key)
376 targetChannel->keys.push_back(makeKey(times.at(key) * 1000.0f,
377 fixQuaternionContinuity(valueAt(key, 1)), valueType));
378 break;
379 case AnimationSampler::Interpolation::Step:
380 // Hold each value until just before the next key
381 for (qsizetype key = 0; key < keyCount; ++key) {
382 const QVector4D value = fixQuaternionContinuity(valueAt(key, 1));
383 targetChannel->keys.push_back(makeKey(times.at(key) * 1000.0f, value, valueType));
384 if (key + 1 < keyCount) {
385 const float holdTime = times.at(key + 1) * 1000.0f - 1.0f;
386 if (holdTime > times.at(key) * 1000.0f)
387 targetChannel->keys.push_back(makeKey(holdTime, value, valueType));
388 }
389 }
390 break;
391 case AnimationSampler::Interpolation::CubicSpline: {
392 // Baked at a fixed rate, with a cap against hostile key times
393 constexpr qsizetype maxBakedKeys = 100000;
394 const float sampleRate = qMax(1.0f, m_options.animationSampleRate);
395 const float startTime = times.first();
396 const float endTime = times.last();
397 if (!qIsFinite(startTime) || !qIsFinite(endTime) || endTime < startTime) {
398 qCWarning(lcQuick3DGltf) << "Ignoring cubic spline animation channel with invalid key times";
399 delete targetChannel;
400 continue;
401 }
402 const double idealSamples = std::ceil(double(endTime - startTime) * double(sampleRate));
403 const qsizetype sampleCount =
404 qMax(qsizetype(1), qsizetype(qMin(idealSamples, double(maxBakedKeys)))) + 1;
405 if (idealSamples > double(maxBakedKeys)) {
406 qCWarning(lcQuick3DGltf) << "Cubic spline animation channel is" << (endTime - startTime)
407 << "seconds long; baking only" << maxBakedKeys << "keys";
408 }
409 qsizetype segment = 0;
410 for (qsizetype sample = 0; sample < sampleCount; ++sample) {
411 const float t = qMin(startTime + sample / sampleRate, endTime);
412 while (segment + 2 < keyCount && t >= times.at(segment + 1))
413 ++segment;
414 const float t0 = times.at(segment);
415 const float t1 = times.at(qMin(segment + 1, keyCount - 1));
416 const float dt = t1 - t0;
417 QVector4D value;
418 if (dt <= 0.0f || segment + 1 >= keyCount) {
419 value = valueAt(segment, 1);
420 } else {
421 // Cubic Hermite spline as the specification defines it
422 const float u = (t - t0) / dt;
423 const float u2 = u * u, u3 = u2 * u;
424 const QVector4D p0 = valueAt(segment, 1);
425 const QVector4D m0 = valueAt(segment, 2) * dt;
426 const QVector4D p1 = valueAt(segment + 1, 1);
427 const QVector4D m1 = valueAt(segment + 1, 0) * dt;
428 value = (2 * u3 - 3 * u2 + 1) * p0 + (u3 - 2 * u2 + u) * m0
429 + (-2 * u3 + 3 * u2) * p1 + (u3 - u2) * m1;
430 }
431 if (isRotation)
432 value.normalize();
433 targetChannel->keys.push_back(makeKey(t * 1000.0f, fixQuaternionContinuity(value), valueType));
434 }
435 break;
436 }
437 }
438
439 if (targetChannel->keys.isEmpty()) {
440 delete targetChannel;
441 continue;
442 }
443
444 targetAnimation.length = qMax(targetAnimation.length, targetChannel->keys.last()->time);
445 targetAnimation.channels.push_back(targetChannel);
446 }
447
448 if (!targetAnimation.channels.isEmpty())
449 m_scene->animations.push_back(new QSSGAnimation(targetAnimation));
450 }
451}
452
453// Only exposes the protected calculateTableEntryFromQuaternion()
458
459// Returns null when there are no usable transform attributes
460QSSGSceneDesc::Instancing *GltfSceneConverter::convertInstancing(const QSSGGltf::Node &source,
461 QSSGSceneDesc::Node &owner)
462{
463 const QList<float> translations = QSSGGltfAccessorReader::readAsFloats(*m_document, source.instanceTranslation);
464 const QList<float> rotations = QSSGGltfAccessorReader::readAsFloats(*m_document, source.instanceRotation);
465 const QList<float> scales = QSSGGltfAccessorReader::readAsFloats(*m_document, source.instanceScale);
466
467 const auto typeMatches = [this](int accessor, QSSGGltf::Accessor::Type expected, const char *what) {
468 if (accessor < 0)
469 return true;
470 const QSSGGltf::Accessor::Type actual = m_document->accessors.at(accessor).type;
471 if (actual == expected)
472 return true;
473 qCWarning(lcQuick3DGltf) << "Ignoring instancing attribute" << what << "with unexpected accessor type";
474 return false;
475 };
476 const bool haveTranslation = typeMatches(source.instanceTranslation, QSSGGltf::Accessor::Type::Vec3,
477 "TRANSLATION");
478 const bool haveRotation = typeMatches(source.instanceRotation, QSSGGltf::Accessor::Type::Vec4, "ROTATION");
479 const bool haveScale = typeMatches(source.instanceScale, QSSGGltf::Accessor::Type::Vec3, "SCALE");
480
481 // Counts must match; use the smallest
482 qsizetype count = -1;
483 const auto considerCount = [&count](qsizetype attributeCount) {
484 if (attributeCount >= 0)
485 count = count < 0 ? attributeCount : qMin(count, attributeCount);
486 };
487 if (source.instanceTranslation >= 0 && haveTranslation)
488 considerCount(translations.size() / 3);
489 if (source.instanceRotation >= 0 && haveRotation)
490 considerCount(rotations.size() / 4);
491 if (source.instanceScale >= 0 && haveScale)
492 considerCount(scales.size() / 3);
493 if (count < 0)
494 return nullptr;
495
496 QByteArray table;
497 table.reserve(count * qsizetype(sizeof(QQuick3DInstancing::InstanceTableEntry)));
498 for (qsizetype i = 0; i < count; ++i) {
499 QVector3D position;
500 QVector3D scale { 1.0f, 1.0f, 1.0f };
501 QQuaternion rotation;
502 if (source.instanceTranslation >= 0 && haveTranslation)
503 position = QVector3D(translations.at(i * 3), translations.at(i * 3 + 1), translations.at(i * 3 + 2));
504 if (source.instanceRotation >= 0 && haveRotation) {
505 // glTF quaternions are (x, y, z, w)
506 rotation = QQuaternion(rotations.at(i * 4 + 3), rotations.at(i * 4), rotations.at(i * 4 + 1),
507 rotations.at(i * 4 + 2));
508 }
509 if (source.instanceScale >= 0 && haveScale)
510 scale = QVector3D(scales.at(i * 3), scales.at(i * 3 + 1), scales.at(i * 3 + 2));
511 const auto entry = InstanceTableEntryBuilder::calculateTableEntryFromQuaternion(position, scale, rotation,
512 QColor(Qt::white), {});
513 table.append(reinterpret_cast<const char *>(&entry), sizeof(entry));
514 }
515
516 auto *instancing = new QSSGSceneDesc::Instancing;
517 if (!source.name.isEmpty())
518 instancing->name = source.name.toUtf8();
519 QSSGSceneDesc::addNode(owner, *instancing);
520 instancing->instanceData = table;
521 instancing->instanceCount = count;
522 return instancing;
523}
524
525void GltfSceneConverter::processNode(int nodeIndex, QSSGSceneDesc::Node &parent)
526{
527 const Node &source = m_document->nodes.at(nodeIndex);
528
529 if (int(source.camera >= 0) + int(source.mesh >= 0) + int(source.light >= 0) > 1) {
530 qCWarning(lcQuick3DGltf) << "Node" << nodeIndex << "has more than one of a camera, mesh and light;"
531 << "only its" << (source.camera >= 0 ? "camera" : "mesh") << "is imported";
532 }
533
534 QSSGSceneDesc::Node *target = createSceneNode(source);
535
536 // The node's transform goes on a parent, so that the model as its own
537 // instance root applies the instance transforms before it
538 QSSGSceneDesc::Node *instancedModel = nullptr;
539 if (source.hasInstancing) {
540 if (target->nodeType == QSSGSceneDesc::Node::Type::Model) {
541 instancedModel = target;
542 target = new QSSGSceneDesc::Node(QSSGSceneDesc::Node::Type::Transform,
543 QSSGSceneDesc::Node::RuntimeType::Node);
544 } else {
545 qCWarning(lcQuick3DGltf) << "Ignoring instancing on node" << nodeIndex << "without a mesh";
546 }
547 }
548
549 QSSGSceneDesc::addNode(parent, *target);
550 m_nodeMap.insert(nodeIndex, target);
551
552 if (instancedModel) {
553 QSSGSceneDesc::addNode(*target, *instancedModel);
554 setModelProperties(static_cast<QSSGSceneDesc::Model &>(*instancedModel), source, nodeIndex);
555 if (QSSGSceneDesc::Instancing *instancing = convertInstancing(source, *instancedModel)) {
556 QSSGSceneDesc::setProperty(*instancedModel, "instancing", &QQuick3DModel::setInstancing, instancing);
557 QSSGSceneDesc::setProperty(*instancedModel, "instanceRoot", &QQuick3DModel::setInstanceRoot,
558 instancedModel);
559 }
560 // The name goes on the parent, which the node's children attach to
561 setNodeProperties(*target, source);
562 for (const int child : source.children)
563 processNode(child, *target);
564 return;
565 }
566
567 // Properties can only be recorded once the node is part of the scene
568 if (source.camera >= 0 && target->nodeType == QSSGSceneDesc::Node::Type::Camera)
569 setCameraProperties(static_cast<QSSGSceneDesc::Camera &>(*target), m_document->cameras.at(source.camera));
570 else if (target->nodeType == QSSGSceneDesc::Node::Type::Model)
571 setModelProperties(static_cast<QSSGSceneDesc::Model &>(*target), source, nodeIndex);
572 else if (source.light >= 0 && target->nodeType == QSSGSceneDesc::Node::Type::Light)
573 setLightProperties(static_cast<QSSGSceneDesc::Light &>(*target), m_document->lights.at(source.light));
574
575 setNodeProperties(*target, source);
576
577 for (const int child : source.children)
578 processNode(child, *target);
579}
580
581QSSGSceneDesc::Node *GltfSceneConverter::createSceneNode(const QSSGGltf::Node &node)
582{
583 if (node.camera >= 0) {
584 const Camera &camera = m_document->cameras.at(node.camera);
585 const auto runtimeType = camera.type == Camera::Type::Orthographic
586 ? QSSGSceneDesc::Node::RuntimeType::OrthographicCamera
587 : QSSGSceneDesc::Node::RuntimeType::PerspectiveCamera;
588 return new QSSGSceneDesc::Camera(runtimeType);
589 }
590
591 if (node.mesh >= 0)
592 return new QSSGSceneDesc::Model();
593
594 if (node.light >= 0) {
595 const Light &light = m_document->lights.at(node.light);
596 auto runtimeType = QSSGSceneDesc::Node::RuntimeType::DirectionalLight;
597 if (light.type == Light::Type::Point)
598 runtimeType = QSSGSceneDesc::Node::RuntimeType::PointLight;
599 else if (light.type == Light::Type::Spot)
600 runtimeType = QSSGSceneDesc::Node::RuntimeType::SpotLight;
601 return new QSSGSceneDesc::Light(runtimeType);
602 }
603
604 return new QSSGSceneDesc::Node(QSSGSceneDesc::Node::Type::Transform, QSSGSceneDesc::Node::RuntimeType::Node);
605}
606
607void GltfSceneConverter::setNodeProperties(QSSGSceneDesc::Node &target, const QSSGGltf::Node &source)
608{
609 if (target.name.isNull()) {
610 if (!source.name.isEmpty())
611 target.name = source.name.toUtf8();
612 else
613 target.name = QSSGQmlUtilities::getQmlElementName(target);
614 }
615
616 QVector3D translation = source.translation;
617 QQuaternion rotation = source.rotation;
618 QVector3D scale = source.scale;
619 if (source.hasMatrix)
620 decomposeMatrix(source.matrix, translation, rotation, scale);
621
622 if (!m_options.designStudioWorkarounds) {
623 QSSGSceneDesc::setProperty(target, "position", &QQuick3DNode::setPosition, translation);
624 } else {
625 QSSGSceneDesc::setProperty(target, "x", &QQuick3DNode::setX, translation.x());
626 QSSGSceneDesc::setProperty(target, "y", &QQuick3DNode::setY, translation.y());
627 QSSGSceneDesc::setProperty(target, "z", &QQuick3DNode::setZ, translation.z());
628 }
629
630 QSSGSceneDesc::setProperty(target, "rotation", &QQuick3DNode::setRotation, rotation);
631 QSSGSceneDesc::setProperty(target, "scale", &QQuick3DNode::setScale, scale);
632
633 // KHR_node_visibility
634 if (!source.visible)
635 QSSGSceneDesc::setProperty(target, "visible", &QQuick3DNode::setVisible, false);
636}
637
638void GltfSceneConverter::setModelProperties(QSSGSceneDesc::Model &target, const QSSGGltf::Node &source, int nodeIndex)
639{
640 const Mesh &mesh = m_document->meshes.at(source.mesh);
641
642 // glTF meshes referenced from several nodes share one mesh resource
643 QSSGSceneDesc::Mesh *meshNode = m_meshMap.value(source.mesh, nullptr);
644 QList<int> usedPrimitives;
645 if (!meshNode) {
646 BuiltMesh &builtMesh = m_builtMeshes[source.mesh];
647 const QList<int> &builtUsedPrimitives = builtMesh.usedPrimitives;
648 const GltfMorphTargetInfo morphInfo = builtMesh.morphInfo;
649 usedPrimitives = builtUsedPrimitives;
650 if (!builtMesh.mesh.isValid()) {
651 qCWarning(lcQuick3DGltf) << "No usable geometry in mesh" << source.mesh;
652 return;
653 }
654 m_scene->meshStorage.push_back(std::move(builtMesh.mesh));
655 const auto idx = m_scene->meshStorage.size() - 1;
656
657 QByteArray meshName = mesh.name.toUtf8();
658 if (meshName.isEmpty())
659 meshName = QByteArrayLiteral("mesh") + QByteArray::number(source.mesh);
660 meshNode = new QSSGSceneDesc::Mesh(meshName, idx);
661 QSSGSceneDesc::addNode(target, *meshNode);
662 m_meshMap.insert(source.mesh, meshNode);
663 m_meshUsedPrimitives.insert(source.mesh, usedPrimitives);
664 m_meshMorphInfo.insert(source.mesh, morphInfo);
665 } else {
666 usedPrimitives = m_meshUsedPrimitives.value(source.mesh);
667 }
668
669 QSSGSceneDesc::setProperty(target, "source", &QQuick3DModel::setSource, QVariant::fromValue(meshNode));
670
671 if (source.skin >= 0) {
672 // Joints are filled in by convertSkins()
673 QSSGSceneDesc::Skin *skinNode = m_skinMap.value(source.skin, nullptr);
674 if (!skinNode) {
675 skinNode = new QSSGSceneDesc::Skin;
676 QSSGSceneDesc::addNode(target, *skinNode);
677 m_skinMap.insert(source.skin, skinNode);
678 }
679 QSSGSceneDesc::setProperty(target, "skin", &QQuick3DModel::setSkin, skinNode);
680 }
681
682 // Node weights override the mesh defaults
683 const GltfMorphTargetInfo morphInfo = m_meshMorphInfo.value(source.mesh);
684 if (morphInfo.count > 0) {
685 QQuick3DMorphTarget::MorphTargetAttributes attributes;
686 if (morphInfo.hasPositions)
687 attributes |= QQuick3DMorphTarget::MorphTargetAttribute::Position;
688 if (morphInfo.hasNormals)
689 attributes |= QQuick3DMorphTarget::MorphTargetAttribute::Normal;
690 if (morphInfo.hasTangents)
691 attributes |= QQuick3DMorphTarget::MorphTargetAttribute::Tangent;
692
693 const QList<float> &weights = source.weights.isEmpty() ? mesh.weights : source.weights;
694 QVarLengthArray<QSSGSceneDesc::MorphTarget *> morphTargets;
695 QList<QSSGSceneDesc::MorphTarget *> morphTargetList;
696 morphTargets.reserve(morphInfo.count);
697 for (int targetIndex = 0; targetIndex < morphInfo.count; ++targetIndex) {
698 auto *morphNode = new QSSGSceneDesc::MorphTarget;
699 QSSGSceneDesc::addNode(target, *morphNode);
700 QSSGSceneDesc::setProperty(*morphNode, "weight", &QQuick3DMorphTarget::setWeight,
701 weights.value(targetIndex, 0.0f));
702 QSSGSceneDesc::setProperty(*morphNode, "attributes", &QQuick3DMorphTarget::setAttributes, attributes);
703 morphTargets.push_back(morphNode);
704 morphTargetList.append(morphNode);
705 }
706 QSSGSceneDesc::setProperty(target, "morphTargets", &QQuick3DModel::morphTargets, morphTargets);
707 m_morphTargetMap.insert(nodeIndex, morphTargetList);
708 }
709
710 // One material per subset, in subset order
711 QVarLengthArray<QSSGSceneDesc::Material *> materials;
712 materials.reserve(usedPrimitives.size());
713 for (const int primitiveIndex : std::as_const(usedPrimitives)) {
714 materials.push_back(ensureMaterial(
715 mesh.primitives.at(primitiveIndex).effectiveMaterial(m_options.materialVariantIndex), target));
716 }
717
718 // Record every variant's materials for models that vary, unless one
719 // variant is baked
720 const bool modelVaries = std::any_of(usedPrimitives.cbegin(), usedPrimitives.cend(),
721 [&mesh](int primitiveIndex) {
722 return !mesh.primitives.at(primitiveIndex).variantMappings.isEmpty();
723 });
724 if (modelVaries && m_options.materialVariant.isEmpty()) {
725 for (QSSGSceneDesc::Material *material : std::as_const(materials))
726 target.defaultMaterials.append(material);
727 for (int variant = 0; variant < m_document->materialVariants.size(); ++variant) {
728 QList<QSSGSceneDesc::Node *> variantList;
729 variantList.reserve(usedPrimitives.size());
730 for (const int primitiveIndex : std::as_const(usedPrimitives))
731 variantList.append(ensureMaterial(mesh.primitives.at(primitiveIndex).effectiveMaterial(variant), target));
732 target.variantMaterials.append(variantList);
733 }
734 }
735
736 if (!materials.isEmpty())
737 QSSGSceneDesc::setProperty(target, "materials", &QQuick3DModel::materials, materials);
738}
739
740QSSGSceneDesc::Material *GltfSceneConverter::ensureMaterial(int materialIndex, QSSGSceneDesc::Node &owner)
741{
742 if (auto *material = m_materialMap.value(materialIndex, nullptr))
743 return material;
744
745 // KHR_materials_pbrSpecularGlossiness
746 auto runtimeType = QSSGSceneDesc::Material::RuntimeType::PrincipledMaterial;
747 if (materialIndex >= 0 && m_document->materials.at(materialIndex).specularGlossiness)
748 runtimeType = QSSGSceneDesc::Material::RuntimeType::SpecularGlossyMaterial;
749
750 auto *material = new QSSGSceneDesc::Material(runtimeType);
751 QSSGSceneDesc::addNode(owner, *material);
752
753 if (materialIndex >= 0) {
754 setMaterialProperties(*material, m_document->materials.at(materialIndex));
755 } else {
756 // The specification's default material: white, fully metallic, fully rough
757 material->name = QByteArrayLiteral("defaultMaterial");
758 QSSGSceneDesc::setProperty(*material, "metalness", &QQuick3DPrincipledMaterial::setMetalness, 1.0f);
759 QSSGSceneDesc::setProperty(*material, "roughness", &QQuick3DPrincipledMaterial::setRoughness, 1.0f);
760 }
761
762 m_materialMap.insert(materialIndex, material);
763 return material;
764}
765
766void GltfSceneConverter::setMaterialProperties(QSSGSceneDesc::Material &target, const QSSGGltf::Material &source)
767{
768 using QSSGSceneDesc::setProperty;
769
770 if (target.name.isNull()) {
771 if (!source.name.isEmpty())
772 target.name = source.name.toUtf8();
773 else
774 target.name = QSSGQmlUtilities::getQmlElementName(target);
775 }
776
777 if (target.runtimeType == QSSGSceneDesc::Material::RuntimeType::SpecularGlossyMaterial) {
778 setSpecularGlossyProperties(target, source);
779 return;
780 }
781
782 // glTF factors are linear, Qt Quick colors are sRGB
783 const QColor baseColor = QSSGUtils::color::linearTosRGB(source.baseColorFactor);
784 setProperty(target, "baseColor", &QQuick3DPrincipledMaterial::setBaseColor, baseColor);
785
786 if (auto *baseColorTexture = ensureTexture(source.baseColorTexture, target)) {
787 setProperty(target, "baseColorMap", &QQuick3DPrincipledMaterial::setBaseColorMap, baseColorTexture);
788 setProperty(target, "opacityChannel", &QQuick3DPrincipledMaterial::setOpacityChannel,
789 QQuick3DPrincipledMaterial::TextureChannelMapping::A);
790 }
791
792 setProperty(target, "metalness", &QQuick3DPrincipledMaterial::setMetalness, source.metallicFactor);
793 setProperty(target, "roughness", &QQuick3DPrincipledMaterial::setRoughness, source.roughnessFactor);
794
795 if (auto *metallicRoughnessTexture = ensureTexture(source.metallicRoughnessTexture, target)) {
796 setProperty(target, "metalnessMap", &QQuick3DPrincipledMaterial::setMetalnessMap, metallicRoughnessTexture);
797 setProperty(target, "metalnessChannel", &QQuick3DPrincipledMaterial::setMetalnessChannel,
798 QQuick3DPrincipledMaterial::TextureChannelMapping::B);
799 setProperty(target, "roughnessMap", &QQuick3DPrincipledMaterial::setRoughnessMap, metallicRoughnessTexture);
800 setProperty(target, "roughnessChannel", &QQuick3DPrincipledMaterial::setRoughnessChannel,
801 QQuick3DPrincipledMaterial::TextureChannelMapping::G);
802 }
803
804 if (auto *normalTexture = ensureTexture(source.normalTexture, target)) {
805 setProperty(target, "normalMap", &QQuick3DPrincipledMaterial::setNormalMap, normalTexture);
806 setProperty(target, "normalStrength", &QQuick3DPrincipledMaterial::setNormalStrength,
807 source.normalTexture.scaleOrStrength);
808 }
809
810 if (auto *occlusionTexture = ensureTexture(source.occlusionTexture, target)) {
811 setProperty(target, "occlusionMap", &QQuick3DPrincipledMaterial::setOcclusionMap, occlusionTexture);
812 setProperty(target, "occlusionChannel", &QQuick3DPrincipledMaterial::setOcclusionChannel,
813 QQuick3DPrincipledMaterial::TextureChannelMapping::R);
814 setProperty(target, "occlusionAmount", &QQuick3DPrincipledMaterial::setOcclusionAmount,
815 source.occlusionTexture.scaleOrStrength);
816 }
817
818 if (auto *emissiveTexture = ensureTexture(source.emissiveTexture, target))
819 setProperty(target, "emissiveMap", &QQuick3DPrincipledMaterial::setEmissiveMap, emissiveTexture);
820 // KHR_materials_emissive_strength
821 const QVector3D emissiveFactor = source.emissiveFactor * source.emissiveStrength.value_or(1.0f);
822 if (!emissiveFactor.isNull())
823 setProperty(target, "emissiveFactor", &QQuick3DPrincipledMaterial::setEmissiveFactor, emissiveFactor);
824
825 if (source.doubleSided) {
826 setProperty(target, "cullMode", &QQuick3DPrincipledMaterial::setCullMode,
827 QQuick3DPrincipledMaterial::CullMode::NoCulling);
828 }
829
830 if (source.alphaMode == QSSGGltf::Material::AlphaMode::Mask) {
831 setProperty(target, "alphaMode", &QQuick3DPrincipledMaterial::setAlphaMode,
832 QQuick3DPrincipledMaterial::AlphaMode::Mask);
833 setProperty(target, "alphaCutoff", &QQuick3DPrincipledMaterial::setAlphaCutoff, source.alphaCutoff);
834 // Masked materials need the opaque prepass depth draw mode to render correctly
835 setProperty(target, "depthDrawMode", &QQuick3DPrincipledMaterial::setDepthDrawMode,
836 QQuick3DMaterial::OpaquePrePassDepthDraw);
837 } else if (source.alphaMode == QSSGGltf::Material::AlphaMode::Blend) {
838 setProperty(target, "alphaMode", &QQuick3DPrincipledMaterial::setAlphaMode,
839 QQuick3DPrincipledMaterial::AlphaMode::Blend);
840 } else {
841 setProperty(target, "alphaMode", &QQuick3DPrincipledMaterial::setAlphaMode,
842 QQuick3DPrincipledMaterial::AlphaMode::Opaque);
843 }
844
845 // KHR_materials_unlit
846 if (source.unlit) {
847 setProperty(target, "lighting", &QQuick3DPrincipledMaterial::setLighting,
848 QQuick3DPrincipledMaterial::Lighting::NoLighting);
849 }
850
851 // KHR_materials_clearcoat
852 if (source.clearcoat) {
853 const QSSGGltf::Material::Clearcoat &clearcoat = *source.clearcoat;
854 setProperty(target, "clearcoatAmount", &QQuick3DPrincipledMaterial::setClearcoatAmount,
855 clearcoat.clearcoatFactor);
856 setProperty(target, "clearcoatRoughnessAmount", &QQuick3DPrincipledMaterial::setClearcoatRoughnessAmount,
857 clearcoat.clearcoatRoughnessFactor);
858 if (auto *clearcoatTexture = ensureTexture(clearcoat.clearcoatTexture, target))
859 setProperty(target, "clearcoatMap", &QQuick3DPrincipledMaterial::setClearcoatMap, clearcoatTexture);
860 if (auto *clearcoatRoughnessTexture = ensureTexture(clearcoat.clearcoatRoughnessTexture, target)) {
861 setProperty(target, "clearcoatRoughnessMap", &QQuick3DPrincipledMaterial::setClearcoatRoughnessMap,
862 clearcoatRoughnessTexture);
863 }
864 if (auto *clearcoatNormalTexture = ensureTexture(clearcoat.clearcoatNormalTexture, target)) {
865 setProperty(target, "clearcoatNormalMap", &QQuick3DPrincipledMaterial::setClearcoatNormalMap,
866 clearcoatNormalTexture);
867 setProperty(target, "clearcoatNormalStrength", &QQuick3DPrincipledMaterial::setClearcoatNormalStrength,
868 clearcoat.clearcoatNormalTexture.scaleOrStrength);
869 }
870 }
871
872 // KHR_materials_transmission
873 if (source.transmission) {
874 setProperty(target, "transmissionFactor", &QQuick3DPrincipledMaterial::setTransmissionFactor,
875 source.transmission->transmissionFactor);
876 if (auto *transmissionTexture = ensureTexture(source.transmission->transmissionTexture, target))
877 setProperty(target, "transmissionMap",
878 &QQuick3DPrincipledMaterial::setTransmissionMap, transmissionTexture);
879 }
880
881 // KHR_materials_volume (only meaningful together with transmission)
882 if (source.volume) {
883 const QSSGGltf::Material::Volume &volume = *source.volume;
884 setProperty(target, "thicknessFactor", &QQuick3DPrincipledMaterial::setThicknessFactor, volume.thicknessFactor);
885 if (auto *thicknessTexture = ensureTexture(volume.thicknessTexture, target))
886 setProperty(target, "thicknessMap", &QQuick3DPrincipledMaterial::setThicknessMap, thicknessTexture);
887 if (volume.attenuationDistance > 0.0f) {
888 setProperty(target, "attenuationDistance", &QQuick3DPrincipledMaterial::setAttenuationDistance,
889 volume.attenuationDistance);
890 }
891 setProperty(target, "attenuationColor", &QQuick3DPrincipledMaterial::setAttenuationColor,
892 QColor::fromRgbF(volume.attenuationColor.x(), volume.attenuationColor.y(),
893 volume.attenuationColor.z()));
894 }
895
896 // KHR_materials_ior
897 if (source.ior)
898 setProperty(target, "indexOfRefraction", &QQuick3DPrincipledMaterial::setIndexOfRefraction, *source.ior);
899
900 // KHR_materials_specular; the color factor and strength alpha are not representable
901 if (source.specular) {
902 const QSSGGltf::Material::Specular &specular = *source.specular;
903 setProperty(target, "specularAmount", &QQuick3DPrincipledMaterial::setSpecularAmount, specular.specularFactor);
904 if (auto *specularColorTexture = ensureTexture(specular.specularColorTexture, target))
905 setProperty(target, "specularMap", &QQuick3DPrincipledMaterial::setSpecularMap, specularColorTexture);
906 if (specular.specularColorFactor != QVector3D(1.0f, 1.0f, 1.0f))
907 qCWarning(lcQuick3DGltf) << "KHR_materials_specular specularColorFactor is not supported";
908 if (specular.specularTexture.isSet())
909 qCWarning(lcQuick3DGltf) << "KHR_materials_specular specularTexture is not supported";
910 }
911
912 // KHR_materials_sheen
913 if (source.sheen) {
914 const QSSGGltf::Material::Sheen &sheen = *source.sheen;
915 setProperty(target,
916 "sheenColor",
917 &QQuick3DPrincipledMaterial::setSheenColor,
918 QSSGUtils::color::linearTosRGB(QVector4D(sheen.sheenColorFactor, 1.0f)));
919 setProperty(target, "sheenRoughness", &QQuick3DPrincipledMaterial::setSheenRoughness, sheen.sheenRoughnessFactor);
920 if (auto *sheenColorTexture = ensureTexture(sheen.sheenColorTexture, target))
921 setProperty(target, "sheenColorMap", &QQuick3DPrincipledMaterial::setSheenColorMap, sheenColorTexture);
922 // Alpha, the default channel
923 if (auto *sheenRoughnessTexture = ensureTexture(sheen.sheenRoughnessTexture, target)) {
924 setProperty(target, "sheenRoughnessMap", &QQuick3DPrincipledMaterial::setSheenRoughnessMap, sheenRoughnessTexture);
925 }
926 }
927
928 // KHR_materials_anisotropy
929 if (source.anisotropy) {
930 const QSSGGltf::Material::Anisotropy &anisotropy = *source.anisotropy;
931 setProperty(target, "anisotropyStrength", &QQuick3DPrincipledMaterial::setAnisotropyStrength, anisotropy.anisotropyStrength);
932 // Radians in glTF, degrees in Qt Quick 3D
933 setProperty(target,
934 "anisotropyRotation",
935 &QQuick3DPrincipledMaterial::setAnisotropyRotation,
936 qRadiansToDegrees(anisotropy.anisotropyRotation));
937 if (auto *anisotropyTexture = ensureTexture(anisotropy.anisotropyTexture, target))
938 setProperty(target, "anisotropyMap", &QQuick3DPrincipledMaterial::setAnisotropyMap, anisotropyTexture);
939 }
940
941 // KHR_materials_iridescence
942 if (source.iridescence) {
943 const QSSGGltf::Material::Iridescence &iridescence = *source.iridescence;
944 setProperty(target, "iridescenceFactor", &QQuick3DPrincipledMaterial::setIridescenceFactor, iridescence.iridescenceFactor);
945 setProperty(target,
946 "iridescenceIndexOfRefraction",
947 &QQuick3DPrincipledMaterial::setIridescenceIndexOfRefraction,
948 iridescence.iridescenceIor);
949 setProperty(target,
950 "iridescenceThicknessMinimum",
951 &QQuick3DPrincipledMaterial::setIridescenceThicknessMinimum,
952 iridescence.iridescenceThicknessMinimum);
953 setProperty(target,
954 "iridescenceThicknessMaximum",
955 &QQuick3DPrincipledMaterial::setIridescenceThicknessMaximum,
956 iridescence.iridescenceThicknessMaximum);
957 // Red and green, the default channels
958 if (auto *iridescenceTexture = ensureTexture(iridescence.iridescenceTexture, target))
959 setProperty(target, "iridescenceMap", &QQuick3DPrincipledMaterial::setIridescenceMap, iridescenceTexture);
960 if (auto *thicknessTexture = ensureTexture(iridescence.iridescenceThicknessTexture, target)) {
961 setProperty(target, "iridescenceThicknessMap", &QQuick3DPrincipledMaterial::setIridescenceThicknessMap, thicknessTexture);
962 }
963 }
964
965 // KHR_materials_dispersion, which requires KHR_materials_volume
966 if (source.dispersion)
967 setProperty(target, "dispersion", &QQuick3DPrincipledMaterial::setDispersion, *source.dispersion);
968}
969
970void GltfSceneConverter::setSpecularGlossyProperties(QSSGSceneDesc::Material &target, const QSSGGltf::Material &source)
971{
972 using QSSGSceneDesc::setProperty;
973
974 const QSSGGltf::Material::SpecularGlossiness &specularGlossiness = *source.specularGlossiness;
975
976 const QColor albedo = QSSGUtils::color::linearTosRGB(specularGlossiness.diffuseFactor);
977 setProperty(target, "albedoColor", &QQuick3DSpecularGlossyMaterial::setAlbedoColor, albedo);
978
979 if (auto *albedoTexture = ensureTexture(specularGlossiness.diffuseTexture, target)) {
980 setProperty(target, "albedoMap", &QQuick3DSpecularGlossyMaterial::setAlbedoMap, albedoTexture);
981 setProperty(target, "opacityChannel", &QQuick3DSpecularGlossyMaterial::setOpacityChannel,
982 QQuick3DSpecularGlossyMaterial::TextureChannelMapping::A);
983 }
984
985 const QColor specularColor = QSSGUtils::color::linearTosRGB(
986 QVector4D(specularGlossiness.specularFactor, 1.0f));
987 setProperty(target, "specularColor", &QQuick3DSpecularGlossyMaterial::setSpecularColor, specularColor);
988 setProperty(target, "glossiness", &QQuick3DSpecularGlossyMaterial::setGlossiness,
989 specularGlossiness.glossinessFactor);
990
991 if (auto *specularGlossinessTexture = ensureTexture(specularGlossiness.specularGlossinessTexture, target)) {
992 setProperty(target, "specularMap", &QQuick3DSpecularGlossyMaterial::setSpecularMap, specularGlossinessTexture);
993 setProperty(target, "glossinessMap",
994 &QQuick3DSpecularGlossyMaterial::setGlossinessMap, specularGlossinessTexture);
995 setProperty(target, "glossinessChannel", &QQuick3DSpecularGlossyMaterial::setGlossinessChannel,
996 QQuick3DSpecularGlossyMaterial::TextureChannelMapping::A);
997 }
998
999 if (auto *normalTexture = ensureTexture(source.normalTexture, target)) {
1000 setProperty(target, "normalMap", &QQuick3DSpecularGlossyMaterial::setNormalMap, normalTexture);
1001 setProperty(target, "normalStrength", &QQuick3DSpecularGlossyMaterial::setNormalStrength,
1002 source.normalTexture.scaleOrStrength);
1003 }
1004
1005 if (auto *occlusionTexture = ensureTexture(source.occlusionTexture, target)) {
1006 setProperty(target, "occlusionMap", &QQuick3DSpecularGlossyMaterial::setOcclusionMap, occlusionTexture);
1007 setProperty(target, "occlusionChannel", &QQuick3DSpecularGlossyMaterial::setOcclusionChannel,
1008 QQuick3DSpecularGlossyMaterial::TextureChannelMapping::R);
1009 setProperty(target, "occlusionAmount", &QQuick3DSpecularGlossyMaterial::setOcclusionAmount,
1010 source.occlusionTexture.scaleOrStrength);
1011 }
1012
1013 if (auto *emissiveTexture = ensureTexture(source.emissiveTexture, target))
1014 setProperty(target, "emissiveMap", &QQuick3DSpecularGlossyMaterial::setEmissiveMap, emissiveTexture);
1015 const QVector3D emissiveFactor = source.emissiveFactor * source.emissiveStrength.value_or(1.0f);
1016 if (!emissiveFactor.isNull())
1017 setProperty(target, "emissiveFactor", &QQuick3DSpecularGlossyMaterial::setEmissiveFactor, emissiveFactor);
1018
1019 if (source.doubleSided) {
1020 setProperty(target, "cullMode", &QQuick3DSpecularGlossyMaterial::setCullMode,
1021 QQuick3DSpecularGlossyMaterial::CullMode::NoCulling);
1022 }
1023
1024 if (source.alphaMode == QSSGGltf::Material::AlphaMode::Mask) {
1025 setProperty(target, "alphaMode", &QQuick3DSpecularGlossyMaterial::setAlphaMode,
1026 QQuick3DSpecularGlossyMaterial::AlphaMode::Mask);
1027 setProperty(target, "alphaCutoff", &QQuick3DSpecularGlossyMaterial::setAlphaCutoff, source.alphaCutoff);
1028 setProperty(target, "depthDrawMode", &QQuick3DSpecularGlossyMaterial::setDepthDrawMode,
1029 QQuick3DMaterial::OpaquePrePassDepthDraw);
1030 } else if (source.alphaMode == QSSGGltf::Material::AlphaMode::Blend) {
1031 setProperty(target, "alphaMode", &QQuick3DSpecularGlossyMaterial::setAlphaMode,
1032 QQuick3DSpecularGlossyMaterial::AlphaMode::Blend);
1033 } else {
1034 setProperty(target, "alphaMode", &QQuick3DSpecularGlossyMaterial::setAlphaMode,
1035 QQuick3DSpecularGlossyMaterial::AlphaMode::Opaque);
1036 }
1037
1038 if (source.unlit) {
1039 setProperty(target, "lighting", &QQuick3DSpecularGlossyMaterial::setLighting,
1040 QQuick3DSpecularGlossyMaterial::Lighting::NoLighting);
1041 }
1042}
1043
1044QSSGSceneDesc::Texture *GltfSceneConverter::ensureTexture(const QSSGGltf::TextureInfo &textureInfo,
1045 QSSGSceneDesc::Node &owner)
1046{
1047 using QSSGSceneDesc::setProperty;
1048
1049 if (!textureInfo.isSet() || textureInfo.index >= m_document->textures.size())
1050 return nullptr;
1051
1052 const QSSGGltf::Texture &texture = m_document->textures.at(textureInfo.index);
1053 // EXT_texture_webp, keeping the regular source as the fallback
1054 const bool webpSupported = isWebpSupported();
1055 const int sourceImage = (texture.webpSource >= 0 && (webpSupported || texture.source < 0))
1056 ? texture.webpSource
1057 : texture.source;
1058 if (sourceImage >= 0 && sourceImage == texture.webpSource && !webpSupported) {
1059 qCWarning(lcQuick3DGltf) << "Texture" << textureInfo.index
1060 << "has only a WebP image, but no WebP image plugin is available";
1061 }
1062 if (sourceImage < 0 || sourceImage >= m_document->images.size()) {
1063 qCWarning(lcQuick3DGltf) << "Texture" << textureInfo.index << "has no image source";
1064 return nullptr;
1065 }
1066
1067 const int texCoord = textureInfo.transform && textureInfo.transform->texCoord >= 0
1068 ? textureInfo.transform->texCoord
1069 : textureInfo.texCoord;
1070
1071 // Textures differing in texCoord or transform cannot be shared
1072 QByteArray key;
1073 key += QByteArray::number(textureInfo.index);
1074 key += ':' + QByteArray::number(texCoord);
1075 if (textureInfo.transform) {
1076 const QSSGGltf::TextureTransform &transform = *textureInfo.transform;
1077 key += ':' + QByteArray::number(transform.offset.x()) + ',' + QByteArray::number(transform.offset.y());
1078 key += ':' + QByteArray::number(transform.scale.x()) + ',' + QByteArray::number(transform.scale.y());
1079 key += ':' + QByteArray::number(transform.rotation);
1080 }
1081 if (auto *cached = m_textureMap.value(key, nullptr))
1082 return cached;
1083
1084 const QSSGGltf::Image &image = m_document->images.at(sourceImage);
1085
1086 // A data: URI would put the whole payload into the name
1087 QByteArray textureName;
1088 if (!QSSGGltfResourceResolver::isDataUri(image.uri))
1089 textureName = image.uri.toUtf8();
1090 if (textureName.isEmpty())
1091 textureName = texture.name.isEmpty() ? image.name.toUtf8() : texture.name.toUtf8();
1092
1093 auto *sceneTexture = new QSSGSceneDesc::Texture(QSSGSceneDesc::Texture::RuntimeType::Image2D, textureName);
1094 QSSGSceneDesc::addNode(owner, *sceneTexture);
1095 m_textureMap.insert(key, sceneTexture);
1096
1097 setProperty(*sceneTexture, "mappingMode", &QQuick3DTexture::setMappingMode, QQuick3DTexture::MappingMode::UV);
1098 if (texCoord > 0) {
1099 // Quick3D supports two texture coordinate sets
1100 setProperty(*sceneTexture, "indexUV", &QQuick3DTexture::setIndexUV, qMin(texCoord, 1));
1101 }
1102
1103 QSSGGltf::Sampler sampler;
1104 if (texture.sampler >= 0 && texture.sampler < m_document->samplers.size())
1105 sampler = m_document->samplers.at(texture.sampler);
1106
1107 static const auto asQtTilingMode = [](int mode) {
1108 switch (mode) {
1109 case QSSGGltf::Sampler::ClampToEdge:
1110 return QQuick3DTexture::TilingMode::ClampToEdge;
1111 case QSSGGltf::Sampler::MirroredRepeat:
1112 return QQuick3DTexture::TilingMode::MirroredRepeat;
1113 case QSSGGltf::Sampler::Repeat:
1114 default:
1115 return QQuick3DTexture::TilingMode::Repeat;
1116 }
1117 };
1118 setProperty(*sceneTexture, "tilingModeHorizontal",
1119 &QQuick3DTexture::setHorizontalTiling, asQtTilingMode(sampler.wrapS));
1120 setProperty(*sceneTexture, "tilingModeVertical",
1121 &QQuick3DTexture::setVerticalTiling, asQtTilingMode(sampler.wrapT));
1122
1123 setProperty(*sceneTexture, "magFilter", &QQuick3DTexture::setMagFilter,
1124 sampler.magFilter == QSSGGltf::Sampler::Nearest ? QQuick3DTexture::Filter::Nearest
1125 : QQuick3DTexture::Filter::Linear);
1126
1127 auto minFilter = QQuick3DTexture::Filter::Linear;
1128 auto mipFilter = m_options.generateMipMaps ? QQuick3DTexture::Filter::Linear : QQuick3DTexture::Filter::None;
1129 switch (sampler.minFilter) {
1130 case QSSGGltf::Sampler::Nearest:
1131 minFilter = QQuick3DTexture::Filter::Nearest;
1132 break;
1133 case QSSGGltf::Sampler::NearestMipMapNearest:
1134 minFilter = QQuick3DTexture::Filter::Nearest;
1135 mipFilter = QQuick3DTexture::Filter::Nearest;
1136 break;
1137 case QSSGGltf::Sampler::LinearMipMapNearest:
1138 mipFilter = QQuick3DTexture::Filter::Nearest;
1139 break;
1140 case QSSGGltf::Sampler::NearestMipMapLinear:
1141 minFilter = QQuick3DTexture::Filter::Nearest;
1142 mipFilter = QQuick3DTexture::Filter::Linear;
1143 break;
1144 case QSSGGltf::Sampler::LinearMipMapLinear:
1145 mipFilter = QQuick3DTexture::Filter::Linear;
1146 break;
1147 case QSSGGltf::Sampler::Linear:
1148 default:
1149 break;
1150 }
1151 setProperty(*sceneTexture, "minFilter", &QQuick3DTexture::setMinFilter, minFilter);
1152 if (mipFilter != QQuick3DTexture::Filter::None) {
1153 setProperty(*sceneTexture, "generateMipmaps", &QQuick3DTexture::setGenerateMipmaps, true);
1154 setProperty(*sceneTexture, "mipFilter", &QQuick3DTexture::setMipFilter, mipFilter);
1155 }
1156
1157 // KHR_texture_transform, applied around the flipped V origin
1158 if (textureInfo.transform) {
1159 const QSSGGltf::TextureTransform &transform = *textureInfo.transform;
1160 setProperty(*sceneTexture, "pivotV", &QQuick3DTexture::setPivotV, 1.0f);
1161 setProperty(*sceneTexture, "positionU", &QQuick3DTexture::setPositionU, transform.offset.x());
1162 setProperty(*sceneTexture, "positionV", &QQuick3DTexture::setPositionV, -transform.offset.y());
1163 setProperty(*sceneTexture, "rotationUV", &QQuick3DTexture::setRotationUV,
1164 float(qRadiansToDegrees(transform.rotation)));
1165 setProperty(*sceneTexture, "scaleU", &QQuick3DTexture::setScaleU, transform.scale.x());
1166 setProperty(*sceneTexture, "scaleV", &QQuick3DTexture::setScaleV, transform.scale.y());
1167 }
1168
1169 // Image data: external file, or embedded in a buffer view or data: URI
1170 const bool isEmbedded = image.bufferView >= 0 || QSSGGltfResourceResolver::isDataUri(image.uri);
1171 if (!isEmbedded) {
1172 QString relativePath = image.uri;
1173 relativePath.replace(QLatin1Char('\\'), QLatin1Char('/'));
1174 const QString resolved = QSSGGltfResourceResolver::resolveFilePath(relativePath, QString());
1175 if (resolved.isEmpty()) {
1176 qCWarning(lcQuick3DGltf) << "Ignoring image URI outside the asset directory:" << image.uri;
1177 return sceneTexture;
1178 }
1179 const QString path = QDir(m_document->baseDir).absoluteFilePath(resolved);
1180 setProperty(*sceneTexture, "source", &QQuick3DTexture::setSource, QUrl { path });
1181 return sceneTexture;
1182 }
1183
1184 QSSGSceneDesc::TextureData *textureData = m_textureDataMap.value(sourceImage, nullptr);
1185 if (!textureData) {
1186 QByteArray imageData;
1187 QString mimeType = image.mimeType;
1188 if (image.bufferView >= 0) {
1189 const QSSGGltf::BufferView &view = m_document->bufferViews.at(image.bufferView);
1190 imageData = m_document->buffers.at(view.buffer).data.mid(view.byteOffset, view.byteLength);
1191 } else {
1192 QString error;
1193 imageData = QSSGGltfResourceResolver::decodeDataUri(image.uri, &error);
1194 if (mimeType.isEmpty()) {
1195 const qsizetype header = image.uri.indexOf(QLatin1Char(','));
1196 // data:<mediatype>[;base64], drop any parameters
1197 mimeType = image.uri.mid(5, header - 5).section(QLatin1Char(';'), 0, 0);
1198 }
1199 }
1200 if (imageData.isEmpty()) {
1201 qCWarning(lcQuick3DGltf) << "Failed to load embedded image" << sourceImage;
1202 return sceneTexture;
1203 }
1204
1205 // The format hint doubles as the file suffix
1206 QByteArray format = mimeType.section(QLatin1Char('/'), 1, 1).trimmed().toLower().toLatin1();
1207 if (format == QByteArrayLiteral("jpeg"))
1208 format = QByteArrayLiteral("jpg");
1209 else if (format.isEmpty())
1210 format = QByteArrayLiteral("png");
1211 textureData = new QSSGSceneDesc::TextureData(imageData, QSize(), format,
1212 quint8(QSSGSceneDesc::TextureData::Flags::Compressed));
1213 QSSGSceneDesc::addNode(*sceneTexture, *textureData);
1214 m_textureDataMap.insert(sourceImage, textureData);
1215 }
1216 setProperty(*sceneTexture, "textureData", &QQuick3DTexture::setTextureData, textureData);
1217
1218 return sceneTexture;
1219}
1220
1221void GltfSceneConverter::setLightProperties(QSSGSceneDesc::Light &target, const QSSGGltf::Light &source)
1222{
1223 using QSSGSceneDesc::setProperty;
1224
1225 // Brightness is the largest component of color * intensity
1226 const QVector3D scaledColor = source.color * source.intensity;
1227 const float brightness = qMax(qMax(1.0f, scaledColor.x()), qMax(scaledColor.y(), scaledColor.z()));
1228 const QColor color = QColor::fromRgbF(scaledColor.x() / brightness, scaledColor.y() / brightness,
1229 scaledColor.z() / brightness);
1230 setProperty(target, "color", &QQuick3DAbstractLight::setColor, color);
1231 setProperty(target, "brightness", &QQuick3DAbstractLight::setBrightness, brightness);
1232
1233 // Inverse square falloff, scaled for Quick3D units
1234 if (source.type == Light::Type::Point) {
1235 setProperty(target, "linearFade", &QQuick3DPointLight::setLinearFade, 0.0f);
1236 setProperty(target, "quadraticFade", &QQuick3DPointLight::setQuadraticFade, 10000.0f);
1237 } else if (source.type == Light::Type::Spot) {
1238 setProperty(target, "linearFade", &QQuick3DSpotLight::setLinearFade, 0.0f);
1239 setProperty(target, "quadraticFade", &QQuick3DSpotLight::setQuadraticFade, 10000.0f);
1240 // glTF cone angles are half angles from the center axis
1241 setProperty(target, "coneAngle", &QQuick3DSpotLight::setConeAngle,
1242 float(qRadiansToDegrees(source.outerConeAngle) * 2.0));
1243 setProperty(target, "innerConeAngle", &QQuick3DSpotLight::setInnerConeAngle,
1244 float(qRadiansToDegrees(source.innerConeAngle) * 2.0));
1245 }
1246}
1247
1248void GltfSceneConverter::setCameraProperties(QSSGSceneDesc::Camera &target, const QSSGGltf::Camera &source)
1249{
1250 using QSSGSceneDesc::setProperty;
1251
1252 if (target.runtimeType == QSSGSceneDesc::Node::RuntimeType::PerspectiveCamera) {
1253 setProperty(target, "clipNear", &QQuick3DPerspectiveCamera::setClipNear, source.znear);
1254 // zfar 0 means infinite in glTF; pick a generous finite plane
1255 const float clipFar = source.zfar > 0.0f ? source.zfar : 10000.0f;
1256 setProperty(target, "clipFar", &QQuick3DPerspectiveCamera::setClipFar, clipFar);
1257 setProperty(target, "fieldOfView", &QQuick3DPerspectiveCamera::setFieldOfView,
1258 float(qRadiansToDegrees(source.yfov)));
1259 } else {
1260 setProperty(target, "clipNear", &QQuick3DOrthographicCamera::setClipNear, source.znear);
1261 // Required by the specification, but not enforced by the parser
1262 const float clipFar = source.zfar > 0.0f ? source.zfar : 10000.0f;
1263 setProperty(target, "clipFar", &QQuick3DOrthographicCamera::setClipFar, clipFar);
1264 // xmag/ymag are half the orthographic view size in scene units
1265 setProperty(target, "horizontalMagnification", &QQuick3DOrthographicCamera::setHorizontalMagnification,
1266 source.xmag * 2.0f);
1267 setProperty(target, "verticalMagnification", &QQuick3DOrthographicCamera::setVerticalMagnification,
1268 source.ymag * 2.0f);
1269 }
1270}
1271
1272QT_END_NAMESPACE
QString convert(const QSSGGltfDocument &document, const QJsonObject &optionsObject, const QFileInfo &sourceFile, QSSGSceneDesc::Scene &targetScene)
Combined button and popup list for selecting options.