Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "gltfsceneconverter.h"
++ #include "gltfmeshbuilder.h"
++
++ #include <QtQuick3DAssetUtils/private/qssgqmlutilities_p.h>
++ #include <QtQuick3DGltf/private/qssggltfparser_p.h>
++
++ #include <QtQuick3D/private/qquick3dmodel_p.h>
++ #include <QtQuick3D/private/qquick3dmorphtarget_p.h>
++ #include <QtQuick3D/private/qquick3dnode_p.h>
++ #include <QtQuick3D/private/qquick3dorthographiccamera_p.h>
++ #include <QtQuick3D/private/qquick3dperspectivecamera_p.h>
++ #include <QtQuick3D/private/qquick3dprincipledmaterial_p.h>
++ #include <QtQuick3D/private/qquick3dskin_p.h>
++ #include <QtQuick3D/private/qquick3dspecularglossymaterial_p.h>
++ #include <QtQuick3D/private/qquick3dtexture_p.h>
++
++ #include <QtQuick3DGltf/private/qssggltfaccessorreader_p.h>
++ #include <QtQuick3DGltf/private/qssggltfresourceresolver_p.h>
++ #include <QtQuick3DUtils/private/qssgutils_p.h>
++
++ #if QT_CONFIG(concurrent)
++ #include <QtConcurrent/qtconcurrentmap.h>
++ #endif
++ #include <QtCore/qdir.h>
++ #include <QtCore/qmath.h>
++ #include <QtGui/qimagereader.h>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace QSSGGltf;
++
++ namespace {
++
++ bool checkBooleanOption(QLatin1StringView optionName, const QJsonObject &options, bool defaultValue = false)
++ {
++ const auto opt = options.constFind(optionName);
++ if (opt == options.constEnd())
++ return defaultValue;
++ const auto value = opt->toObject().value(QLatin1String("value"));
++ return value.toBool(defaultValue);
++ }
++
++ float getRealOption(QLatin1StringView optionName, const QJsonObject &options, float defaultValue)
++ {
++ const auto opt = options.constFind(optionName);
++ if (opt == options.constEnd())
++ return defaultValue;
++ const auto value = opt->toObject().value(QLatin1String("value"));
++ return float(value.toDouble(defaultValue));
++ }
++
++ QString getStringOption(QLatin1StringView optionName, const QJsonObject &options)
++ {
++ const auto opt = options.constFind(optionName);
++ if (opt == options.constEnd())
++ return {};
++ return opt->toObject().value(QLatin1String("value")).toString();
++ }
++
++ void decomposeMatrix(const QMatrix4x4 &matrix, QVector3D &translation, QQuaternion &rotation, QVector3D &scale)
++ {
++ translation = matrix.column(3).toVector3D();
++
++ QVector3D columns[3] = { matrix.column(0).toVector3D(),
++ matrix.column(1).toVector3D(),
++ matrix.column(2).toVector3D() };
++ scale = QVector3D(columns[0].length(), columns[1].length(), columns[2].length());
++
++ // Fold a mirroring into one scale axis
++ if (matrix.determinant() < 0.0f)
++ scale.setX(-scale.x());
++
++ for (int i = 0; i < 3; ++i) {
++ const float axisScale = i == 0 ? scale.x() : (i == 1 ? scale.y() : scale.z());
++ if (!qFuzzyIsNull(axisScale))
++ columns[i] /= axisScale;
++ }
++
++ const float rotationValues[9] = {
++ columns[0].x(), columns[1].x(), columns[2].x(),
++ columns[0].y(), columns[1].y(), columns[2].y(),
++ columns[0].z(), columns[1].z(), columns[2].z()
++ };
++ rotation = QQuaternion::fromRotationMatrix(QMatrix3x3(rotationValues)).normalized();
++ }
++
++ // The WebP plugin is in qtimageformats, which is not necessarily deployed
++ bool isWebpSupported()
++ {
++ static const bool supported = QImageReader::supportedImageFormats().contains(QByteArrayLiteral("webp"));
++ return supported;
++ }
++
++ } // namespace
++
++ GltfSceneConverter::Options GltfSceneConverter::parseOptions(const QJsonObject &optionsObject)
++ {
++ // Options may arrive wrapped in an "options" object (balsam) or flat
++ QJsonObject options = optionsObject;
++ if (auto it = options.constFind(QLatin1String("options")); it != options.constEnd())
++ options = it->toObject();
++
++ Options result;
++ result.designStudioWorkarounds = checkBooleanOption(QLatin1StringView("designStudioWorkarounds"), options);
++ result.joinIdenticalVertices = checkBooleanOption(QLatin1StringView("joinIdenticalVertices"), options, true);
++ result.useFloatJointIndices = checkBooleanOption(QLatin1StringView("useFloatJointIndices"), options);
++ result.generateSmoothNormals = checkBooleanOption(QLatin1StringView("generateSmoothNormals"), options);
++ result.forceTangentGeneration = checkBooleanOption(QLatin1StringView("calculateTangentSpace"), options);
++ result.generateMipMaps = checkBooleanOption(QLatin1StringView("generateMipMaps"), options);
++ result.animationSampleRate = getRealOption(QLatin1StringView("animationSampleRate"), options, 30.0f);
++ result.materialVariant = getStringOption(QLatin1StringView("materialVariant"), options);
++
++ result.generateMeshLODs = checkBooleanOption(QLatin1StringView("generateMeshLevelsOfDetail"), options);
++ if (result.generateMeshLODs) {
++ if (checkBooleanOption(QLatin1StringView("recalculateLodNormals"), options)) {
++ const float mergeAngle =
++ getRealOption(QLatin1StringView("recalculateLodNormalsMergeAngle"), options, 60.0f);
++ result.lodNormalMergeAngle = qBound(0.0f, mergeAngle, 270.0f);
++ const float splitAngle =
++ getRealOption(QLatin1StringView("recalculateLodNormalsSplitAngle"), options, 25.0f);
++ result.lodNormalSplitAngle = qBound(0.0f, splitAngle, 270.0f);
++ } else {
++ result.lodNormalMergeAngle = 0.0f;
++ result.lodNormalSplitAngle = 0.0f;
++ }
++ }
++
++ if (checkBooleanOption(QLatin1StringView("globalScale"), options))
++ result.globalScaleValue = getRealOption(QLatin1StringView("globalScaleValue"), options, 1.0f);
++
++ return result;
++ }
++
++ QString GltfSceneConverter::convert(const QSSGGltfDocument &document, const QJsonObject &optionsObject,
++ const QFileInfo &sourceFile, QSSGSceneDesc::Scene &targetScene)
++ {
++ m_document = &document;
++ m_scene = &targetScene;
++ m_options = parseOptions(optionsObject);
++ if (!m_options.materialVariant.isEmpty()) {
++ m_options.materialVariantIndex = int(document.materialVariants.indexOf(m_options.materialVariant));
++ if (m_options.materialVariantIndex < 0) {
++ // Not an error, since one option may be applied to a batch of assets
++ qCWarning(lcQuick3DGltf) << "Asset has no material variant" << m_options.materialVariant
++ << "- available variants:" << document.materialVariants;
++ }
++ }
++ // Baking one variant leaves nothing to switch between
++ if (m_options.materialVariant.isEmpty())
++ targetScene.materialVariants = document.materialVariants;
++ m_nodeMap.clear();
++ m_meshMap.clear();
++ m_materialMap.clear();
++ m_textureMap.clear();
++ m_textureDataMap.clear();
++ m_skinMap.clear();
++ m_meshUsedPrimitives.clear();
++ m_meshMorphInfo.clear();
++ m_morphTargetMap.clear();
++ m_builtMeshes.clear();
++
++ targetScene.sourceDir = sourceFile.path();
++ // For simplicity, and convenience, we'll just use the file path as the id.
++ targetScene.id = sourceFile.canonicalFilePath();
++
++ if (!targetScene.root) {
++ auto root = new QSSGSceneDesc::Node(QByteArrayLiteral("Root"), QSSGSceneDesc::Node::Type::Transform,
++ QSSGSceneDesc::Node::RuntimeType::Node);
++ QSSGSceneDesc::addNode(targetScene, *root);
++ }
++
++ if (!qFuzzyCompare(m_options.globalScaleValue, 1.0f) && !qFuzzyIsNull(m_options.globalScaleValue)) {
++ const float gscale = m_options.globalScaleValue;
++ QSSGSceneDesc::setProperty(*targetScene.root, "scale", &QQuick3DNode::setScale,
++ QVector3D { gscale, gscale, gscale });
++ }
++
++ int sceneIndex = document.scene;
++ if (sceneIndex < 0 && !document.scenes.isEmpty()) {
++ qCWarning(lcQuick3DGltf) << "Document has no default scene, using scene 0";
++ sceneIndex = 0;
++ }
++ if (sceneIndex < 0)
++ return QStringLiteral("Document contains no scene");
++
++ if (document.extensionsRequired.contains(QLatin1String("EXT_texture_webp")) && !isWebpSupported())
++ return QStringLiteral("Asset requires EXT_texture_webp, but no WebP image plugin is available");
++
++ // Mesh building only reads the document, so it runs in parallel
++ {
++ QList<int> meshIndexes(document.meshes.size());
++ std::iota(meshIndexes.begin(), meshIndexes.end(), 0);
++ const auto buildOneMesh = [&](int meshIndex) {
++ BuiltMesh result;
++ result.mesh = GltfMeshBuilder::buildMesh(document, document.meshes.at(meshIndex), m_options,
++ &result.usedPrimitives, &result.morphInfo);
++ return result;
++ };
++ #if QT_CONFIG(concurrent)
++ QList<BuiltMesh> built = QtConcurrent::blockingMapped(meshIndexes, buildOneMesh);
++ #else
++ QList<BuiltMesh> built;
++ built.reserve(meshIndexes.size());
++ for (const int meshIndex : meshIndexes)
++ built.append(buildOneMesh(meshIndex));
++ #endif
++ for (qsizetype meshIndex = 0; meshIndex < built.size(); ++meshIndex)
++ m_builtMeshes.insert(int(meshIndex), std::move(built[meshIndex]));
++ }
++
++ for (const int nodeIndex : document.scenes.at(sceneIndex).nodes)
++ processNode(nodeIndex, *targetScene.root);
++
++ // Skins can only be resolved once all joint nodes exist
++ convertSkins();
++ convertAnimations();
++
++ return QString();
++ }
++
++ void GltfSceneConverter::convertSkins()
++ {
++ for (auto it = m_skinMap.constBegin(); it != m_skinMap.constEnd(); ++it) {
++ const Skin &source = m_document->skins.at(it.key());
++ QSSGSceneDesc::Skin *skinNode = it.value();
++
++ QVarLengthArray<QSSGSceneDesc::Node *> joints;
++ QList<QMatrix4x4> inverseBindPoses;
++ joints.reserve(source.joints.size());
++
++ const QList<float> ibmData = QSSGGltfAccessorReader::readAsFloats(*m_document, source.inverseBindMatrices);
++ const bool hasBindMatrices = ibmData.size() >= source.joints.size() * 16;
++
++ for (qsizetype jointIndex = 0; jointIndex < source.joints.size(); ++jointIndex) {
++ QSSGSceneDesc::Node *jointNode = m_nodeMap.value(source.joints.at(jointIndex), nullptr);
++ if (!jointNode) {
++ qCWarning(lcQuick3DGltf) << "Skin" << it.key() << "joint" << jointIndex
++ << "references a node outside the scene";
++ continue;
++ }
++ joints.push_back(jointNode);
++ if (hasBindMatrices) {
++ // glTF matrices are column-major; QMatrix4x4(float*) is row-major
++ inverseBindPoses.append(QMatrix4x4(ibmData.constData() + jointIndex * 16).transposed());
++ } else {
++ inverseBindPoses.append(QMatrix4x4());
++ }
++ }
++
++ QSSGSceneDesc::setProperty(*skinNode, "joints", &QQuick3DSkin::joints, joints);
++ QSSGSceneDesc::setProperty(*skinNode, "inverseBindPoses", &QQuick3DSkin::setInverseBindPoses, inverseBindPoses);
++ }
++ }
++
++ void GltfSceneConverter::convertAnimations()
++ {
++ using QSSGAnimation = QSSGSceneDesc::Animation;
++
++ const auto makeKey = [](float timeMs, const QVector4D &value, QSSGAnimation::KeyPosition::ValueType valueType) {
++ const auto flag = quint16(quint16(QSSGAnimation::KeyPosition::KeyType::Time) | quint16(valueType));
++ return new QSSGAnimation::KeyPosition { value, timeMs, flag };
++ };
++
++ for (qsizetype animationIndex = 0; animationIndex < m_document->animations.size(); ++animationIndex) {
++ const Animation &sourceAnimation = m_document->animations.at(animationIndex);
++
++ QSSGAnimation targetAnimation;
++ targetAnimation.name = sourceAnimation.name.isEmpty()
++ ? QByteArrayLiteral("animation") + QByteArray::number(animationIndex)
++ : sourceAnimation.name.toUtf8();
++ // Key times are in milliseconds
++ targetAnimation.framesPerSecond = 1000.0f;
++
++ for (const AnimationChannel &channel : sourceAnimation.channels) {
++ QSSGSceneDesc::Node *targetNode = m_nodeMap.value(channel.targetNode, nullptr);
++ if (!targetNode)
++ continue;
++
++ if (channel.path == AnimationChannel::Path::Weights) {
++ // Weights are interleaved per key across all targets
++ const QList<QSSGSceneDesc::MorphTarget *> morphTargets = m_morphTargetMap.value(channel.targetNode);
++ if (morphTargets.isEmpty())
++ continue;
++ const Node &sourceNode = m_document->nodes.at(channel.targetNode);
++ int sourceTargetCount = 0;
++ for (const MeshPrimitive &primitive : m_document->meshes.at(sourceNode.mesh).primitives)
++ sourceTargetCount = qMax(sourceTargetCount, int(primitive.targets.size()));
++ if (sourceTargetCount == 0)
++ continue;
++
++ const AnimationSampler &weightSampler = sourceAnimation.samplers.at(channel.sampler);
++ // CUBICSPLINE stores in-tangent, value, out-tangent per key
++ const bool isCubic = weightSampler.interpolation == AnimationSampler::Interpolation::CubicSpline;
++ if (isCubic)
++ qCWarning(lcQuick3DGltf) << "Cubic spline morph weight animation is not supported, using linear";
++ const bool isStep = weightSampler.interpolation == AnimationSampler::Interpolation::Step;
++ const int valuesPerKey = isCubic ? 3 : 1;
++ const QList<float> times = QSSGGltfAccessorReader::readAsFloats(*m_document, weightSampler.input);
++ const QList<float> values = QSSGGltfAccessorReader::readAsFloats(*m_document, weightSampler.output);
++ const qsizetype keyCount =
++ qMin(qsizetype(times.size()), values.size() / (sourceTargetCount * valuesPerKey));
++
++ for (qsizetype targetIndex = 0; targetIndex < morphTargets.size(); ++targetIndex) {
++ auto *weightChannel = new QSSGAnimation::Channel;
++ weightChannel->target = morphTargets.at(targetIndex);
++ weightChannel->targetProperty = QSSGAnimation::Channel::TargetProperty::Weight;
++ for (qsizetype key = 0; key < keyCount; ++key) {
++ const qsizetype block = key * valuesPerKey + (isCubic ? 1 : 0);
++ const QVector4D weight(values.value(block * sourceTargetCount + targetIndex), 0.0f, 0.0f, 0.0f);
++ weightChannel->keys.push_back(makeKey(times.at(key) * 1000.0f, weight,
++ QSSGAnimation::KeyPosition::ValueType::Number));
++ if (isStep && key + 1 < keyCount) {
++ const float holdTime = times.at(key + 1) * 1000.0f - 1.0f;
++ if (holdTime > times.at(key) * 1000.0f) {
++ weightChannel->keys.push_back(makeKey(holdTime, weight,
++ QSSGAnimation::KeyPosition::ValueType::Number));
++ }
++ }
++ }
++ if (weightChannel->keys.isEmpty()) {
++ delete weightChannel;
++ continue;
++ }
++ targetAnimation.length = qMax(targetAnimation.length, weightChannel->keys.last()->time);
++ targetAnimation.channels.push_back(weightChannel);
++ }
++ continue;
++ }
++
++ const AnimationSampler &sampler = sourceAnimation.samplers.at(channel.sampler);
++ const QList<float> times = QSSGGltfAccessorReader::readAsFloats(*m_document, sampler.input);
++ const QList<float> values = QSSGGltfAccessorReader::readAsFloats(*m_document, sampler.output);
++ if (times.isEmpty())
++ continue;
++
++ const bool isRotation = channel.path == AnimationChannel::Path::Rotation;
++ const int components = isRotation ? 4 : 3;
++ const auto valueType = isRotation ? QSSGAnimation::KeyPosition::ValueType::Quaternion
++ : QSSGAnimation::KeyPosition::ValueType::Vec3;
++
++ // CUBICSPLINE stores in-tangent, value, out-tangent per key
++ const int valuesPerKey = sampler.interpolation == AnimationSampler::Interpolation::CubicSpline ? 3 : 1;
++ const qsizetype keyCount = qMin(qsizetype(times.size()), values.size() / (components * valuesPerKey));
++ if (keyCount == 0)
++ continue;
++
++ const auto valueAt = [&](qsizetype key, int part = 0) {
++ // part: 0 in-tangent, 1 value, 2 out-tangent
++ const qsizetype base = (key * valuesPerKey + (valuesPerKey == 3 ? part : 0)) * components;
++ return QVector4D(values.value(base), values.value(base + 1), values.value(base + 2),
++ components == 4 ? values.value(base + 3) : 0.0f);
++ };
++ // Keep consecutive keys in one hemisphere for shortest-path interpolation
++ QVector4D previousValue;
++ const auto fixQuaternionContinuity = [&](QVector4D value) {
++ if (isRotation && QVector4D::dotProduct(value, previousValue) < 0.0f)
++ value = -value;
++ previousValue = value;
++ return value;
++ };
++
++ auto *targetChannel = new QSSGAnimation::Channel;
++ targetChannel->target = targetNode;
++ targetChannel->targetProperty = channel.path == AnimationChannel::Path::Translation
++ ? QSSGAnimation::Channel::TargetProperty::Position
++ : (isRotation ? QSSGAnimation::Channel::TargetProperty::Rotation
++ : QSSGAnimation::Channel::TargetProperty::Scale);
++
++ switch (sampler.interpolation) {
++ case AnimationSampler::Interpolation::Linear:
++ for (qsizetype key = 0; key < keyCount; ++key)
++ targetChannel->keys.push_back(makeKey(times.at(key) * 1000.0f,
++ fixQuaternionContinuity(valueAt(key, 1)), valueType));
++ break;
++ case AnimationSampler::Interpolation::Step:
++ // Hold each value until just before the next key
++ for (qsizetype key = 0; key < keyCount; ++key) {
++ const QVector4D value = fixQuaternionContinuity(valueAt(key, 1));
++ targetChannel->keys.push_back(makeKey(times.at(key) * 1000.0f, value, valueType));
++ if (key + 1 < keyCount) {
++ const float holdTime = times.at(key + 1) * 1000.0f - 1.0f;
++ if (holdTime > times.at(key) * 1000.0f)
++ targetChannel->keys.push_back(makeKey(holdTime, value, valueType));
++ }
++ }
++ break;
++ case AnimationSampler::Interpolation::CubicSpline: {
++ // Baked at a fixed rate, with a cap against hostile key times
++ constexpr qsizetype maxBakedKeys = 100000;
++ const float sampleRate = qMax(1.0f, m_options.animationSampleRate);
++ const float startTime = times.first();
++ const float endTime = times.last();
++ if (!qIsFinite(startTime) || !qIsFinite(endTime) || endTime < startTime) {
++ qCWarning(lcQuick3DGltf) << "Ignoring cubic spline animation channel with invalid key times";
++ delete targetChannel;
++ continue;
++ }
++ const double idealSamples = std::ceil(double(endTime - startTime) * double(sampleRate));
++ const qsizetype sampleCount =
++ qMax(qsizetype(1), qsizetype(qMin(idealSamples, double(maxBakedKeys)))) + 1;
++ if (idealSamples > double(maxBakedKeys)) {
++ qCWarning(lcQuick3DGltf) << "Cubic spline animation channel is" << (endTime - startTime)
++ << "seconds long; baking only" << maxBakedKeys << "keys";
++ }
++ qsizetype segment = 0;
++ for (qsizetype sample = 0; sample < sampleCount; ++sample) {
++ const float t = qMin(startTime + sample / sampleRate, endTime);
++ while (segment + 2 < keyCount && t >= times.at(segment + 1))
++ ++segment;
++ const float t0 = times.at(segment);
++ const float t1 = times.at(qMin(segment + 1, keyCount - 1));
++ const float dt = t1 - t0;
++ QVector4D value;
++ if (dt <= 0.0f || segment + 1 >= keyCount) {
++ value = valueAt(segment, 1);
++ } else {
++ // Cubic Hermite spline as the specification defines it
++ const float u = (t - t0) / dt;
++ const float u2 = u * u, u3 = u2 * u;
++ const QVector4D p0 = valueAt(segment, 1);
++ const QVector4D m0 = valueAt(segment, 2) * dt;
++ const QVector4D p1 = valueAt(segment + 1, 1);
++ const QVector4D m1 = valueAt(segment + 1, 0) * dt;
++ value = (2 * u3 - 3 * u2 + 1) * p0 + (u3 - 2 * u2 + u) * m0
++ + (-2 * u3 + 3 * u2) * p1 + (u3 - u2) * m1;
++ }
++ if (isRotation)
++ value.normalize();
++ targetChannel->keys.push_back(makeKey(t * 1000.0f, fixQuaternionContinuity(value), valueType));
++ }
++ break;
++ }
++ }
++
++ if (targetChannel->keys.isEmpty()) {
++ delete targetChannel;
++ continue;
++ }
++
++ targetAnimation.length = qMax(targetAnimation.length, targetChannel->keys.last()->time);
++ targetAnimation.channels.push_back(targetChannel);
++ }
++
++ if (!targetAnimation.channels.isEmpty())
++ m_scene->animations.push_back(new QSSGAnimation(targetAnimation));
++ }
++ }
++
++ // Only exposes the protected calculateTableEntryFromQuaternion()
++ struct InstanceTableEntryBuilder : QQuick3DInstancing
++ {
++ using QQuick3DInstancing::calculateTableEntryFromQuaternion;
++ };
++
++ // Returns null when there are no usable transform attributes
++ QSSGSceneDesc::Instancing *GltfSceneConverter::convertInstancing(const QSSGGltf::Node &source,
++ QSSGSceneDesc::Node &owner)
++ {
++ const QList<float> translations = QSSGGltfAccessorReader::readAsFloats(*m_document, source.instanceTranslation);
++ const QList<float> rotations = QSSGGltfAccessorReader::readAsFloats(*m_document, source.instanceRotation);
++ const QList<float> scales = QSSGGltfAccessorReader::readAsFloats(*m_document, source.instanceScale);
++
++ const auto typeMatches = [this](int accessor, QSSGGltf::Accessor::Type expected, const char *what) {
++ if (accessor < 0)
++ return true;
++ const QSSGGltf::Accessor::Type actual = m_document->accessors.at(accessor).type;
++ if (actual == expected)
++ return true;
++ qCWarning(lcQuick3DGltf) << "Ignoring instancing attribute" << what << "with unexpected accessor type";
++ return false;
++ };
++ const bool haveTranslation = typeMatches(source.instanceTranslation, QSSGGltf::Accessor::Type::Vec3,
++ "TRANSLATION");
++ const bool haveRotation = typeMatches(source.instanceRotation, QSSGGltf::Accessor::Type::Vec4, "ROTATION");
++ const bool haveScale = typeMatches(source.instanceScale, QSSGGltf::Accessor::Type::Vec3, "SCALE");
++
++ // Counts must match; use the smallest
++ qsizetype count = -1;
++ const auto considerCount = [&count](qsizetype attributeCount) {
++ if (attributeCount >= 0)
++ count = count < 0 ? attributeCount : qMin(count, attributeCount);
++ };
++ if (source.instanceTranslation >= 0 && haveTranslation)
++ considerCount(translations.size() / 3);
++ if (source.instanceRotation >= 0 && haveRotation)
++ considerCount(rotations.size() / 4);
++ if (source.instanceScale >= 0 && haveScale)
++ considerCount(scales.size() / 3);
++ if (count < 0)
++ return nullptr;
++
++ QByteArray table;
++ table.reserve(count * qsizetype(sizeof(QQuick3DInstancing::InstanceTableEntry)));
++ for (qsizetype i = 0; i < count; ++i) {
++ QVector3D position;
++ QVector3D scale { 1.0f, 1.0f, 1.0f };
++ QQuaternion rotation;
++ if (source.instanceTranslation >= 0 && haveTranslation)
++ position = QVector3D(translations.at(i * 3), translations.at(i * 3 + 1), translations.at(i * 3 + 2));
++ if (source.instanceRotation >= 0 && haveRotation) {
++ // glTF quaternions are (x, y, z, w)
++ rotation = QQuaternion(rotations.at(i * 4 + 3), rotations.at(i * 4), rotations.at(i * 4 + 1),
++ rotations.at(i * 4 + 2));
++ }
++ if (source.instanceScale >= 0 && haveScale)
++ scale = QVector3D(scales.at(i * 3), scales.at(i * 3 + 1), scales.at(i * 3 + 2));
++ const auto entry = InstanceTableEntryBuilder::calculateTableEntryFromQuaternion(position, scale, rotation,
++ QColor(Qt::white), {});
++ table.append(reinterpret_cast<const char *>(&entry), sizeof(entry));
++ }
++
++ auto *instancing = new QSSGSceneDesc::Instancing;
++ if (!source.name.isEmpty())
++ instancing->name = source.name.toUtf8();
++ QSSGSceneDesc::addNode(owner, *instancing);
++ instancing->instanceData = table;
++ instancing->instanceCount = count;
++ return instancing;
++ }
++
++ void GltfSceneConverter::processNode(int nodeIndex, QSSGSceneDesc::Node &parent)
++ {
++ const Node &source = m_document->nodes.at(nodeIndex);
++
++ if (int(source.camera >= 0) + int(source.mesh >= 0) + int(source.light >= 0) > 1) {
++ qCWarning(lcQuick3DGltf) << "Node" << nodeIndex << "has more than one of a camera, mesh and light;"
++ << "only its" << (source.camera >= 0 ? "camera" : "mesh") << "is imported";
++ }
++
++ QSSGSceneDesc::Node *target = createSceneNode(source);
++
++ // The node's transform goes on a parent, so that the model as its own
++ // instance root applies the instance transforms before it
++ QSSGSceneDesc::Node *instancedModel = nullptr;
++ if (source.hasInstancing) {
++ if (target->nodeType == QSSGSceneDesc::Node::Type::Model) {
++ instancedModel = target;
++ target = new QSSGSceneDesc::Node(QSSGSceneDesc::Node::Type::Transform,
++ QSSGSceneDesc::Node::RuntimeType::Node);
++ } else {
++ qCWarning(lcQuick3DGltf) << "Ignoring instancing on node" << nodeIndex << "without a mesh";
++ }
++ }
++
++ QSSGSceneDesc::addNode(parent, *target);
++ m_nodeMap.insert(nodeIndex, target);
++
++ if (instancedModel) {
++ QSSGSceneDesc::addNode(*target, *instancedModel);
++ setModelProperties(static_cast<QSSGSceneDesc::Model &>(*instancedModel), source, nodeIndex);
++ if (QSSGSceneDesc::Instancing *instancing = convertInstancing(source, *instancedModel)) {
++ QSSGSceneDesc::setProperty(*instancedModel, "instancing", &QQuick3DModel::setInstancing, instancing);
++ QSSGSceneDesc::setProperty(*instancedModel, "instanceRoot", &QQuick3DModel::setInstanceRoot,
++ instancedModel);
++ }
++ // The name goes on the parent, which the node's children attach to
++ setNodeProperties(*target, source);
++ for (const int child : source.children)
++ processNode(child, *target);
++ return;
++ }
++
++ // Properties can only be recorded once the node is part of the scene
++ if (source.camera >= 0 && target->nodeType == QSSGSceneDesc::Node::Type::Camera)
++ setCameraProperties(static_cast<QSSGSceneDesc::Camera &>(*target), m_document->cameras.at(source.camera));
++ else if (target->nodeType == QSSGSceneDesc::Node::Type::Model)
++ setModelProperties(static_cast<QSSGSceneDesc::Model &>(*target), source, nodeIndex);
++ else if (source.light >= 0 && target->nodeType == QSSGSceneDesc::Node::Type::Light)
++ setLightProperties(static_cast<QSSGSceneDesc::Light &>(*target), m_document->lights.at(source.light));
++
++ setNodeProperties(*target, source);
++
++ for (const int child : source.children)
++ processNode(child, *target);
++ }
++
++ QSSGSceneDesc::Node *GltfSceneConverter::createSceneNode(const QSSGGltf::Node &node)
++ {
++ if (node.camera >= 0) {
++ const Camera &camera = m_document->cameras.at(node.camera);
++ const auto runtimeType = camera.type == Camera::Type::Orthographic
++ ? QSSGSceneDesc::Node::RuntimeType::OrthographicCamera
++ : QSSGSceneDesc::Node::RuntimeType::PerspectiveCamera;
++ return new QSSGSceneDesc::Camera(runtimeType);
++ }
++
++ if (node.mesh >= 0)
++ return new QSSGSceneDesc::Model();
++
++ if (node.light >= 0) {
++ const Light &light = m_document->lights.at(node.light);
++ auto runtimeType = QSSGSceneDesc::Node::RuntimeType::DirectionalLight;
++ if (light.type == Light::Type::Point)
++ runtimeType = QSSGSceneDesc::Node::RuntimeType::PointLight;
++ else if (light.type == Light::Type::Spot)
++ runtimeType = QSSGSceneDesc::Node::RuntimeType::SpotLight;
++ return new QSSGSceneDesc::Light(runtimeType);
++ }
++
++ return new QSSGSceneDesc::Node(QSSGSceneDesc::Node::Type::Transform, QSSGSceneDesc::Node::RuntimeType::Node);
++ }
++
++ void GltfSceneConverter::setNodeProperties(QSSGSceneDesc::Node &target, const QSSGGltf::Node &source)
++ {
++ if (target.name.isNull()) {
++ if (!source.name.isEmpty())
++ target.name = source.name.toUtf8();
++ else
++ target.name = QSSGQmlUtilities::getQmlElementName(target);
++ }
++
++ QVector3D translation = source.translation;
++ QQuaternion rotation = source.rotation;
++ QVector3D scale = source.scale;
++ if (source.hasMatrix)
++ decomposeMatrix(source.matrix, translation, rotation, scale);
++
++ if (!m_options.designStudioWorkarounds) {
++ QSSGSceneDesc::setProperty(target, "position", &QQuick3DNode::setPosition, translation);
++ } else {
++ QSSGSceneDesc::setProperty(target, "x", &QQuick3DNode::setX, translation.x());
++ QSSGSceneDesc::setProperty(target, "y", &QQuick3DNode::setY, translation.y());
++ QSSGSceneDesc::setProperty(target, "z", &QQuick3DNode::setZ, translation.z());
++ }
++
++ QSSGSceneDesc::setProperty(target, "rotation", &QQuick3DNode::setRotation, rotation);
++ QSSGSceneDesc::setProperty(target, "scale", &QQuick3DNode::setScale, scale);
++
++ // KHR_node_visibility
++ if (!source.visible)
++ QSSGSceneDesc::setProperty(target, "visible", &QQuick3DNode::setVisible, false);
++ }
++
++ void GltfSceneConverter::setModelProperties(QSSGSceneDesc::Model &target, const QSSGGltf::Node &source, int nodeIndex)
++ {
++ const Mesh &mesh = m_document->meshes.at(source.mesh);
++
++ // glTF meshes referenced from several nodes share one mesh resource
++ QSSGSceneDesc::Mesh *meshNode = m_meshMap.value(source.mesh, nullptr);
++ QList<int> usedPrimitives;
++ if (!meshNode) {
++ BuiltMesh &builtMesh = m_builtMeshes[source.mesh];
++ const QList<int> &builtUsedPrimitives = builtMesh.usedPrimitives;
++ const GltfMorphTargetInfo morphInfo = builtMesh.morphInfo;
++ usedPrimitives = builtUsedPrimitives;
++ if (!builtMesh.mesh.isValid()) {
++ qCWarning(lcQuick3DGltf) << "No usable geometry in mesh" << source.mesh;
++ return;
++ }
++ m_scene->meshStorage.push_back(std::move(builtMesh.mesh));
++ const auto idx = m_scene->meshStorage.size() - 1;
++
++ QByteArray meshName = mesh.name.toUtf8();
++ if (meshName.isEmpty())
++ meshName = QByteArrayLiteral("mesh") + QByteArray::number(source.mesh);
++ meshNode = new QSSGSceneDesc::Mesh(meshName, idx);
++ QSSGSceneDesc::addNode(target, *meshNode);
++ m_meshMap.insert(source.mesh, meshNode);
++ m_meshUsedPrimitives.insert(source.mesh, usedPrimitives);
++ m_meshMorphInfo.insert(source.mesh, morphInfo);
++ } else {
++ usedPrimitives = m_meshUsedPrimitives.value(source.mesh);
++ }
++
++ QSSGSceneDesc::setProperty(target, "source", &QQuick3DModel::setSource, QVariant::fromValue(meshNode));
++
++ if (source.skin >= 0) {
++ // Joints are filled in by convertSkins()
++ QSSGSceneDesc::Skin *skinNode = m_skinMap.value(source.skin, nullptr);
++ if (!skinNode) {
++ skinNode = new QSSGSceneDesc::Skin;
++ QSSGSceneDesc::addNode(target, *skinNode);
++ m_skinMap.insert(source.skin, skinNode);
++ }
++ QSSGSceneDesc::setProperty(target, "skin", &QQuick3DModel::setSkin, skinNode);
++ }
++
++ // Node weights override the mesh defaults
++ const GltfMorphTargetInfo morphInfo = m_meshMorphInfo.value(source.mesh);
++ if (morphInfo.count > 0) {
++ QQuick3DMorphTarget::MorphTargetAttributes attributes;
++ if (morphInfo.hasPositions)
++ attributes |= QQuick3DMorphTarget::MorphTargetAttribute::Position;
++ if (morphInfo.hasNormals)
++ attributes |= QQuick3DMorphTarget::MorphTargetAttribute::Normal;
++ if (morphInfo.hasTangents)
++ attributes |= QQuick3DMorphTarget::MorphTargetAttribute::Tangent;
++
++ const QList<float> &weights = source.weights.isEmpty() ? mesh.weights : source.weights;
++ QVarLengthArray<QSSGSceneDesc::MorphTarget *> morphTargets;
++ QList<QSSGSceneDesc::MorphTarget *> morphTargetList;
++ morphTargets.reserve(morphInfo.count);
++ for (int targetIndex = 0; targetIndex < morphInfo.count; ++targetIndex) {
++ auto *morphNode = new QSSGSceneDesc::MorphTarget;
++ QSSGSceneDesc::addNode(target, *morphNode);
++ QSSGSceneDesc::setProperty(*morphNode, "weight", &QQuick3DMorphTarget::setWeight,
++ weights.value(targetIndex, 0.0f));
++ QSSGSceneDesc::setProperty(*morphNode, "attributes", &QQuick3DMorphTarget::setAttributes, attributes);
++ morphTargets.push_back(morphNode);
++ morphTargetList.append(morphNode);
++ }
++ QSSGSceneDesc::setProperty(target, "morphTargets", &QQuick3DModel::morphTargets, morphTargets);
++ m_morphTargetMap.insert(nodeIndex, morphTargetList);
++ }
++
++ // One material per subset, in subset order
++ QVarLengthArray<QSSGSceneDesc::Material *> materials;
++ materials.reserve(usedPrimitives.size());
++ for (const int primitiveIndex : std::as_const(usedPrimitives)) {
++ materials.push_back(ensureMaterial(
++ mesh.primitives.at(primitiveIndex).effectiveMaterial(m_options.materialVariantIndex), target));
++ }
++
++ // Record every variant's materials for models that vary, unless one
++ // variant is baked
++ const bool modelVaries = std::any_of(usedPrimitives.cbegin(), usedPrimitives.cend(),
++ [&mesh](int primitiveIndex) {
++ return !mesh.primitives.at(primitiveIndex).variantMappings.isEmpty();
++ });
++ if (modelVaries && m_options.materialVariant.isEmpty()) {
++ for (QSSGSceneDesc::Material *material : std::as_const(materials))
++ target.defaultMaterials.append(material);
++ for (int variant = 0; variant < m_document->materialVariants.size(); ++variant) {
++ QList<QSSGSceneDesc::Node *> variantList;
++ variantList.reserve(usedPrimitives.size());
++ for (const int primitiveIndex : std::as_const(usedPrimitives))
++ variantList.append(ensureMaterial(mesh.primitives.at(primitiveIndex).effectiveMaterial(variant), target));
++ target.variantMaterials.append(variantList);
++ }
++ }
++
++ if (!materials.isEmpty())
++ QSSGSceneDesc::setProperty(target, "materials", &QQuick3DModel::materials, materials);
++ }
++
++ QSSGSceneDesc::Material *GltfSceneConverter::ensureMaterial(int materialIndex, QSSGSceneDesc::Node &owner)
++ {
++ if (auto *material = m_materialMap.value(materialIndex, nullptr))
++ return material;
++
++ // KHR_materials_pbrSpecularGlossiness
++ auto runtimeType = QSSGSceneDesc::Material::RuntimeType::PrincipledMaterial;
++ if (materialIndex >= 0 && m_document->materials.at(materialIndex).specularGlossiness)
++ runtimeType = QSSGSceneDesc::Material::RuntimeType::SpecularGlossyMaterial;
++
++ auto *material = new QSSGSceneDesc::Material(runtimeType);
++ QSSGSceneDesc::addNode(owner, *material);
++
++ if (materialIndex >= 0) {
++ setMaterialProperties(*material, m_document->materials.at(materialIndex));
++ } else {
++ // The specification's default material: white, fully metallic, fully rough
++ material->name = QByteArrayLiteral("defaultMaterial");
++ QSSGSceneDesc::setProperty(*material, "metalness", &QQuick3DPrincipledMaterial::setMetalness, 1.0f);
++ QSSGSceneDesc::setProperty(*material, "roughness", &QQuick3DPrincipledMaterial::setRoughness, 1.0f);
++ }
++
++ m_materialMap.insert(materialIndex, material);
++ return material;
++ }
++
++ void GltfSceneConverter::setMaterialProperties(QSSGSceneDesc::Material &target, const QSSGGltf::Material &source)
++ {
++ using QSSGSceneDesc::setProperty;
++
++ if (target.name.isNull()) {
++ if (!source.name.isEmpty())
++ target.name = source.name.toUtf8();
++ else
++ target.name = QSSGQmlUtilities::getQmlElementName(target);
++ }
++
++ if (target.runtimeType == QSSGSceneDesc::Material::RuntimeType::SpecularGlossyMaterial) {
++ setSpecularGlossyProperties(target, source);
++ return;
++ }
++
++ // glTF factors are linear, Qt Quick colors are sRGB
++ const QColor baseColor = QSSGUtils::color::linearTosRGB(source.baseColorFactor);
++ setProperty(target, "baseColor", &QQuick3DPrincipledMaterial::setBaseColor, baseColor);
++
++ if (auto *baseColorTexture = ensureTexture(source.baseColorTexture, target)) {
++ setProperty(target, "baseColorMap", &QQuick3DPrincipledMaterial::setBaseColorMap, baseColorTexture);
++ setProperty(target, "opacityChannel", &QQuick3DPrincipledMaterial::setOpacityChannel,
++ QQuick3DPrincipledMaterial::TextureChannelMapping::A);
++ }
++
++ setProperty(target, "metalness", &QQuick3DPrincipledMaterial::setMetalness, source.metallicFactor);
++ setProperty(target, "roughness", &QQuick3DPrincipledMaterial::setRoughness, source.roughnessFactor);
++
++ if (auto *metallicRoughnessTexture = ensureTexture(source.metallicRoughnessTexture, target)) {
++ setProperty(target, "metalnessMap", &QQuick3DPrincipledMaterial::setMetalnessMap, metallicRoughnessTexture);
++ setProperty(target, "metalnessChannel", &QQuick3DPrincipledMaterial::setMetalnessChannel,
++ QQuick3DPrincipledMaterial::TextureChannelMapping::B);
++ setProperty(target, "roughnessMap", &QQuick3DPrincipledMaterial::setRoughnessMap, metallicRoughnessTexture);
++ setProperty(target, "roughnessChannel", &QQuick3DPrincipledMaterial::setRoughnessChannel,
++ QQuick3DPrincipledMaterial::TextureChannelMapping::G);
++ }
++
++ if (auto *normalTexture = ensureTexture(source.normalTexture, target)) {
++ setProperty(target, "normalMap", &QQuick3DPrincipledMaterial::setNormalMap, normalTexture);
++ setProperty(target, "normalStrength", &QQuick3DPrincipledMaterial::setNormalStrength,
++ source.normalTexture.scaleOrStrength);
++ }
++
++ if (auto *occlusionTexture = ensureTexture(source.occlusionTexture, target)) {
++ setProperty(target, "occlusionMap", &QQuick3DPrincipledMaterial::setOcclusionMap, occlusionTexture);
++ setProperty(target, "occlusionChannel", &QQuick3DPrincipledMaterial::setOcclusionChannel,
++ QQuick3DPrincipledMaterial::TextureChannelMapping::R);
++ setProperty(target, "occlusionAmount", &QQuick3DPrincipledMaterial::setOcclusionAmount,
++ source.occlusionTexture.scaleOrStrength);
++ }
++
++ if (auto *emissiveTexture = ensureTexture(source.emissiveTexture, target))
++ setProperty(target, "emissiveMap", &QQuick3DPrincipledMaterial::setEmissiveMap, emissiveTexture);
++ // KHR_materials_emissive_strength
++ const QVector3D emissiveFactor = source.emissiveFactor * source.emissiveStrength.value_or(1.0f);
++ if (!emissiveFactor.isNull())
++ setProperty(target, "emissiveFactor", &QQuick3DPrincipledMaterial::setEmissiveFactor, emissiveFactor);
++
++ if (source.doubleSided) {
++ setProperty(target, "cullMode", &QQuick3DPrincipledMaterial::setCullMode,
++ QQuick3DPrincipledMaterial::CullMode::NoCulling);
++ }
++
++ if (source.alphaMode == QSSGGltf::Material::AlphaMode::Mask) {
++ setProperty(target, "alphaMode", &QQuick3DPrincipledMaterial::setAlphaMode,
++ QQuick3DPrincipledMaterial::AlphaMode::Mask);
++ setProperty(target, "alphaCutoff", &QQuick3DPrincipledMaterial::setAlphaCutoff, source.alphaCutoff);
++ // Masked materials need the opaque prepass depth draw mode to render correctly
++ setProperty(target, "depthDrawMode", &QQuick3DPrincipledMaterial::setDepthDrawMode,
++ QQuick3DMaterial::OpaquePrePassDepthDraw);
++ } else if (source.alphaMode == QSSGGltf::Material::AlphaMode::Blend) {
++ setProperty(target, "alphaMode", &QQuick3DPrincipledMaterial::setAlphaMode,
++ QQuick3DPrincipledMaterial::AlphaMode::Blend);
++ } else {
++ setProperty(target, "alphaMode", &QQuick3DPrincipledMaterial::setAlphaMode,
++ QQuick3DPrincipledMaterial::AlphaMode::Opaque);
++ }
++
++ // KHR_materials_unlit
++ if (source.unlit) {
++ setProperty(target, "lighting", &QQuick3DPrincipledMaterial::setLighting,
++ QQuick3DPrincipledMaterial::Lighting::NoLighting);
++ }
++
++ // KHR_materials_clearcoat
++ if (source.clearcoat) {
++ const QSSGGltf::Material::Clearcoat &clearcoat = *source.clearcoat;
++ setProperty(target, "clearcoatAmount", &QQuick3DPrincipledMaterial::setClearcoatAmount,
++ clearcoat.clearcoatFactor);
++ setProperty(target, "clearcoatRoughnessAmount", &QQuick3DPrincipledMaterial::setClearcoatRoughnessAmount,
++ clearcoat.clearcoatRoughnessFactor);
++ if (auto *clearcoatTexture = ensureTexture(clearcoat.clearcoatTexture, target))
++ setProperty(target, "clearcoatMap", &QQuick3DPrincipledMaterial::setClearcoatMap, clearcoatTexture);
++ if (auto *clearcoatRoughnessTexture = ensureTexture(clearcoat.clearcoatRoughnessTexture, target)) {
++ setProperty(target, "clearcoatRoughnessMap", &QQuick3DPrincipledMaterial::setClearcoatRoughnessMap,
++ clearcoatRoughnessTexture);
++ }
++ if (auto *clearcoatNormalTexture = ensureTexture(clearcoat.clearcoatNormalTexture, target)) {
++ setProperty(target, "clearcoatNormalMap", &QQuick3DPrincipledMaterial::setClearcoatNormalMap,
++ clearcoatNormalTexture);
++ setProperty(target, "clearcoatNormalStrength", &QQuick3DPrincipledMaterial::setClearcoatNormalStrength,
++ clearcoat.clearcoatNormalTexture.scaleOrStrength);
++ }
++ }
++
++ // KHR_materials_transmission
++ if (source.transmission) {
++ setProperty(target, "transmissionFactor", &QQuick3DPrincipledMaterial::setTransmissionFactor,
++ source.transmission->transmissionFactor);
++ if (auto *transmissionTexture = ensureTexture(source.transmission->transmissionTexture, target))
++ setProperty(target, "transmissionMap",
++ &QQuick3DPrincipledMaterial::setTransmissionMap, transmissionTexture);
++ }
++
++ // KHR_materials_volume (only meaningful together with transmission)
++ if (source.volume) {
++ const QSSGGltf::Material::Volume &volume = *source.volume;
++ setProperty(target, "thicknessFactor", &QQuick3DPrincipledMaterial::setThicknessFactor, volume.thicknessFactor);
++ if (auto *thicknessTexture = ensureTexture(volume.thicknessTexture, target))
++ setProperty(target, "thicknessMap", &QQuick3DPrincipledMaterial::setThicknessMap, thicknessTexture);
++ if (volume.attenuationDistance > 0.0f) {
++ setProperty(target, "attenuationDistance", &QQuick3DPrincipledMaterial::setAttenuationDistance,
++ volume.attenuationDistance);
++ }
++ setProperty(target, "attenuationColor", &QQuick3DPrincipledMaterial::setAttenuationColor,
++ QColor::fromRgbF(volume.attenuationColor.x(), volume.attenuationColor.y(),
++ volume.attenuationColor.z()));
++ }
++
++ // KHR_materials_ior
++ if (source.ior)
++ setProperty(target, "indexOfRefraction", &QQuick3DPrincipledMaterial::setIndexOfRefraction, *source.ior);
++
++ // KHR_materials_specular; the color factor and strength alpha are not representable
++ if (source.specular) {
++ const QSSGGltf::Material::Specular &specular = *source.specular;
++ setProperty(target, "specularAmount", &QQuick3DPrincipledMaterial::setSpecularAmount, specular.specularFactor);
++ if (auto *specularColorTexture = ensureTexture(specular.specularColorTexture, target))
++ setProperty(target, "specularMap", &QQuick3DPrincipledMaterial::setSpecularMap, specularColorTexture);
++ if (specular.specularColorFactor != QVector3D(1.0f, 1.0f, 1.0f))
++ qCWarning(lcQuick3DGltf) << "KHR_materials_specular specularColorFactor is not supported";
++ if (specular.specularTexture.isSet())
++ qCWarning(lcQuick3DGltf) << "KHR_materials_specular specularTexture is not supported";
++ }
++
++ // KHR_materials_sheen
++ if (source.sheen) {
++ const QSSGGltf::Material::Sheen &sheen = *source.sheen;
++ setProperty(target,
++ "sheenColor",
++ &QQuick3DPrincipledMaterial::setSheenColor,
++ QSSGUtils::color::linearTosRGB(QVector4D(sheen.sheenColorFactor, 1.0f)));
++ setProperty(target, "sheenRoughness", &QQuick3DPrincipledMaterial::setSheenRoughness, sheen.sheenRoughnessFactor);
++ if (auto *sheenColorTexture = ensureTexture(sheen.sheenColorTexture, target))
++ setProperty(target, "sheenColorMap", &QQuick3DPrincipledMaterial::setSheenColorMap, sheenColorTexture);
++ // Alpha, the default channel
++ if (auto *sheenRoughnessTexture = ensureTexture(sheen.sheenRoughnessTexture, target)) {
++ setProperty(target, "sheenRoughnessMap", &QQuick3DPrincipledMaterial::setSheenRoughnessMap, sheenRoughnessTexture);
++ }
++ }
++
++ // KHR_materials_anisotropy
++ if (source.anisotropy) {
++ const QSSGGltf::Material::Anisotropy &anisotropy = *source.anisotropy;
++ setProperty(target, "anisotropyStrength", &QQuick3DPrincipledMaterial::setAnisotropyStrength, anisotropy.anisotropyStrength);
++ // Radians in glTF, degrees in Qt Quick 3D
++ setProperty(target,
++ "anisotropyRotation",
++ &QQuick3DPrincipledMaterial::setAnisotropyRotation,
++ qRadiansToDegrees(anisotropy.anisotropyRotation));
++ if (auto *anisotropyTexture = ensureTexture(anisotropy.anisotropyTexture, target))
++ setProperty(target, "anisotropyMap", &QQuick3DPrincipledMaterial::setAnisotropyMap, anisotropyTexture);
++ }
++
++ // KHR_materials_iridescence
++ if (source.iridescence) {
++ const QSSGGltf::Material::Iridescence &iridescence = *source.iridescence;
++ setProperty(target, "iridescenceFactor", &QQuick3DPrincipledMaterial::setIridescenceFactor, iridescence.iridescenceFactor);
++ setProperty(target,
++ "iridescenceIndexOfRefraction",
++ &QQuick3DPrincipledMaterial::setIridescenceIndexOfRefraction,
++ iridescence.iridescenceIor);
++ setProperty(target,
++ "iridescenceThicknessMinimum",
++ &QQuick3DPrincipledMaterial::setIridescenceThicknessMinimum,
++ iridescence.iridescenceThicknessMinimum);
++ setProperty(target,
++ "iridescenceThicknessMaximum",
++ &QQuick3DPrincipledMaterial::setIridescenceThicknessMaximum,
++ iridescence.iridescenceThicknessMaximum);
++ // Red and green, the default channels
++ if (auto *iridescenceTexture = ensureTexture(iridescence.iridescenceTexture, target))
++ setProperty(target, "iridescenceMap", &QQuick3DPrincipledMaterial::setIridescenceMap, iridescenceTexture);
++ if (auto *thicknessTexture = ensureTexture(iridescence.iridescenceThicknessTexture, target)) {
++ setProperty(target, "iridescenceThicknessMap", &QQuick3DPrincipledMaterial::setIridescenceThicknessMap, thicknessTexture);
++ }
++ }
++
++ // KHR_materials_dispersion, which requires KHR_materials_volume
++ if (source.dispersion)
++ setProperty(target, "dispersion", &QQuick3DPrincipledMaterial::setDispersion, *source.dispersion);
++ }
++
++ void GltfSceneConverter::setSpecularGlossyProperties(QSSGSceneDesc::Material &target, const QSSGGltf::Material &source)
++ {
++ using QSSGSceneDesc::setProperty;
++
++ const QSSGGltf::Material::SpecularGlossiness &specularGlossiness = *source.specularGlossiness;
++
++ const QColor albedo = QSSGUtils::color::linearTosRGB(specularGlossiness.diffuseFactor);
++ setProperty(target, "albedoColor", &QQuick3DSpecularGlossyMaterial::setAlbedoColor, albedo);
++
++ if (auto *albedoTexture = ensureTexture(specularGlossiness.diffuseTexture, target)) {
++ setProperty(target, "albedoMap", &QQuick3DSpecularGlossyMaterial::setAlbedoMap, albedoTexture);
++ setProperty(target, "opacityChannel", &QQuick3DSpecularGlossyMaterial::setOpacityChannel,
++ QQuick3DSpecularGlossyMaterial::TextureChannelMapping::A);
++ }
++
++ const QColor specularColor = QSSGUtils::color::linearTosRGB(
++ QVector4D(specularGlossiness.specularFactor, 1.0f));
++ setProperty(target, "specularColor", &QQuick3DSpecularGlossyMaterial::setSpecularColor, specularColor);
++ setProperty(target, "glossiness", &QQuick3DSpecularGlossyMaterial::setGlossiness,
++ specularGlossiness.glossinessFactor);
++
++ if (auto *specularGlossinessTexture = ensureTexture(specularGlossiness.specularGlossinessTexture, target)) {
++ setProperty(target, "specularMap", &QQuick3DSpecularGlossyMaterial::setSpecularMap, specularGlossinessTexture);
++ setProperty(target, "glossinessMap",
++ &QQuick3DSpecularGlossyMaterial::setGlossinessMap, specularGlossinessTexture);
++ setProperty(target, "glossinessChannel", &QQuick3DSpecularGlossyMaterial::setGlossinessChannel,
++ QQuick3DSpecularGlossyMaterial::TextureChannelMapping::A);
++ }
++
++ if (auto *normalTexture = ensureTexture(source.normalTexture, target)) {
++ setProperty(target, "normalMap", &QQuick3DSpecularGlossyMaterial::setNormalMap, normalTexture);
++ setProperty(target, "normalStrength", &QQuick3DSpecularGlossyMaterial::setNormalStrength,
++ source.normalTexture.scaleOrStrength);
++ }
++
++ if (auto *occlusionTexture = ensureTexture(source.occlusionTexture, target)) {
++ setProperty(target, "occlusionMap", &QQuick3DSpecularGlossyMaterial::setOcclusionMap, occlusionTexture);
++ setProperty(target, "occlusionChannel", &QQuick3DSpecularGlossyMaterial::setOcclusionChannel,
++ QQuick3DSpecularGlossyMaterial::TextureChannelMapping::R);
++ setProperty(target, "occlusionAmount", &QQuick3DSpecularGlossyMaterial::setOcclusionAmount,
++ source.occlusionTexture.scaleOrStrength);
++ }
++
++ if (auto *emissiveTexture = ensureTexture(source.emissiveTexture, target))
++ setProperty(target, "emissiveMap", &QQuick3DSpecularGlossyMaterial::setEmissiveMap, emissiveTexture);
++ const QVector3D emissiveFactor = source.emissiveFactor * source.emissiveStrength.value_or(1.0f);
++ if (!emissiveFactor.isNull())
++ setProperty(target, "emissiveFactor", &QQuick3DSpecularGlossyMaterial::setEmissiveFactor, emissiveFactor);
++
++ if (source.doubleSided) {
++ setProperty(target, "cullMode", &QQuick3DSpecularGlossyMaterial::setCullMode,
++ QQuick3DSpecularGlossyMaterial::CullMode::NoCulling);
++ }
++
++ if (source.alphaMode == QSSGGltf::Material::AlphaMode::Mask) {
++ setProperty(target, "alphaMode", &QQuick3DSpecularGlossyMaterial::setAlphaMode,
++ QQuick3DSpecularGlossyMaterial::AlphaMode::Mask);
++ setProperty(target, "alphaCutoff", &QQuick3DSpecularGlossyMaterial::setAlphaCutoff, source.alphaCutoff);
++ setProperty(target, "depthDrawMode", &QQuick3DSpecularGlossyMaterial::setDepthDrawMode,
++ QQuick3DMaterial::OpaquePrePassDepthDraw);
++ } else if (source.alphaMode == QSSGGltf::Material::AlphaMode::Blend) {
++ setProperty(target, "alphaMode", &QQuick3DSpecularGlossyMaterial::setAlphaMode,
++ QQuick3DSpecularGlossyMaterial::AlphaMode::Blend);
++ } else {
++ setProperty(target, "alphaMode", &QQuick3DSpecularGlossyMaterial::setAlphaMode,
++ QQuick3DSpecularGlossyMaterial::AlphaMode::Opaque);
++ }
++
++ if (source.unlit) {
++ setProperty(target, "lighting", &QQuick3DSpecularGlossyMaterial::setLighting,
++ QQuick3DSpecularGlossyMaterial::Lighting::NoLighting);
++ }
++ }
++
++ QSSGSceneDesc::Texture *GltfSceneConverter::ensureTexture(const QSSGGltf::TextureInfo &textureInfo,
++ QSSGSceneDesc::Node &owner)
++ {
++ using QSSGSceneDesc::setProperty;
++
++ if (!textureInfo.isSet() || textureInfo.index >= m_document->textures.size())
++ return nullptr;
++
++ const QSSGGltf::Texture &texture = m_document->textures.at(textureInfo.index);
++ // EXT_texture_webp, keeping the regular source as the fallback
++ const bool webpSupported = isWebpSupported();
++ const int sourceImage = (texture.webpSource >= 0 && (webpSupported || texture.source < 0))
++ ? texture.webpSource
++ : texture.source;
++ if (sourceImage >= 0 && sourceImage == texture.webpSource && !webpSupported) {
++ qCWarning(lcQuick3DGltf) << "Texture" << textureInfo.index
++ << "has only a WebP image, but no WebP image plugin is available";
++ }
++ if (sourceImage < 0 || sourceImage >= m_document->images.size()) {
++ qCWarning(lcQuick3DGltf) << "Texture" << textureInfo.index << "has no image source";
++ return nullptr;
++ }
++
++ const int texCoord = textureInfo.transform && textureInfo.transform->texCoord >= 0
++ ? textureInfo.transform->texCoord
++ : textureInfo.texCoord;
++
++ // Textures differing in texCoord or transform cannot be shared
++ QByteArray key;
++ key += QByteArray::number(textureInfo.index);
++ key += ':' + QByteArray::number(texCoord);
++ if (textureInfo.transform) {
++ const QSSGGltf::TextureTransform &transform = *textureInfo.transform;
++ key += ':' + QByteArray::number(transform.offset.x()) + ',' + QByteArray::number(transform.offset.y());
++ key += ':' + QByteArray::number(transform.scale.x()) + ',' + QByteArray::number(transform.scale.y());
++ key += ':' + QByteArray::number(transform.rotation);
++ }
++ if (auto *cached = m_textureMap.value(key, nullptr))
++ return cached;
++
++ const QSSGGltf::Image &image = m_document->images.at(sourceImage);
++
++ // A data: URI would put the whole payload into the name
++ QByteArray textureName;
++ if (!QSSGGltfResourceResolver::isDataUri(image.uri))
++ textureName = image.uri.toUtf8();
++ if (textureName.isEmpty())
++ textureName = texture.name.isEmpty() ? image.name.toUtf8() : texture.name.toUtf8();
++
++ auto *sceneTexture = new QSSGSceneDesc::Texture(QSSGSceneDesc::Texture::RuntimeType::Image2D, textureName);
++ QSSGSceneDesc::addNode(owner, *sceneTexture);
++ m_textureMap.insert(key, sceneTexture);
++
++ setProperty(*sceneTexture, "mappingMode", &QQuick3DTexture::setMappingMode, QQuick3DTexture::MappingMode::UV);
++ if (texCoord > 0) {
++ // Quick3D supports two texture coordinate sets
++ setProperty(*sceneTexture, "indexUV", &QQuick3DTexture::setIndexUV, qMin(texCoord, 1));
++ }
++
++ QSSGGltf::Sampler sampler;
++ if (texture.sampler >= 0 && texture.sampler < m_document->samplers.size())
++ sampler = m_document->samplers.at(texture.sampler);
++
++ static const auto asQtTilingMode = [](int mode) {
++ switch (mode) {
++ case QSSGGltf::Sampler::ClampToEdge:
++ return QQuick3DTexture::TilingMode::ClampToEdge;
++ case QSSGGltf::Sampler::MirroredRepeat:
++ return QQuick3DTexture::TilingMode::MirroredRepeat;
++ case QSSGGltf::Sampler::Repeat:
++ default:
++ return QQuick3DTexture::TilingMode::Repeat;
++ }
++ };
++ setProperty(*sceneTexture, "tilingModeHorizontal",
++ &QQuick3DTexture::setHorizontalTiling, asQtTilingMode(sampler.wrapS));
++ setProperty(*sceneTexture, "tilingModeVertical",
++ &QQuick3DTexture::setVerticalTiling, asQtTilingMode(sampler.wrapT));
++
++ setProperty(*sceneTexture, "magFilter", &QQuick3DTexture::setMagFilter,
++ sampler.magFilter == QSSGGltf::Sampler::Nearest ? QQuick3DTexture::Filter::Nearest
++ : QQuick3DTexture::Filter::Linear);
++
++ auto minFilter = QQuick3DTexture::Filter::Linear;
++ auto mipFilter = m_options.generateMipMaps ? QQuick3DTexture::Filter::Linear : QQuick3DTexture::Filter::None;
++ switch (sampler.minFilter) {
++ case QSSGGltf::Sampler::Nearest:
++ minFilter = QQuick3DTexture::Filter::Nearest;
++ break;
++ case QSSGGltf::Sampler::NearestMipMapNearest:
++ minFilter = QQuick3DTexture::Filter::Nearest;
++ mipFilter = QQuick3DTexture::Filter::Nearest;
++ break;
++ case QSSGGltf::Sampler::LinearMipMapNearest:
++ mipFilter = QQuick3DTexture::Filter::Nearest;
++ break;
++ case QSSGGltf::Sampler::NearestMipMapLinear:
++ minFilter = QQuick3DTexture::Filter::Nearest;
++ mipFilter = QQuick3DTexture::Filter::Linear;
++ break;
++ case QSSGGltf::Sampler::LinearMipMapLinear:
++ mipFilter = QQuick3DTexture::Filter::Linear;
++ break;
++ case QSSGGltf::Sampler::Linear:
++ default:
++ break;
++ }
++ setProperty(*sceneTexture, "minFilter", &QQuick3DTexture::setMinFilter, minFilter);
++ if (mipFilter != QQuick3DTexture::Filter::None) {
++ setProperty(*sceneTexture, "generateMipmaps", &QQuick3DTexture::setGenerateMipmaps, true);
++ setProperty(*sceneTexture, "mipFilter", &QQuick3DTexture::setMipFilter, mipFilter);
++ }
++
++ // KHR_texture_transform, applied around the flipped V origin
++ if (textureInfo.transform) {
++ const QSSGGltf::TextureTransform &transform = *textureInfo.transform;
++ setProperty(*sceneTexture, "pivotV", &QQuick3DTexture::setPivotV, 1.0f);
++ setProperty(*sceneTexture, "positionU", &QQuick3DTexture::setPositionU, transform.offset.x());
++ setProperty(*sceneTexture, "positionV", &QQuick3DTexture::setPositionV, -transform.offset.y());
++ setProperty(*sceneTexture, "rotationUV", &QQuick3DTexture::setRotationUV,
++ float(qRadiansToDegrees(transform.rotation)));
++ setProperty(*sceneTexture, "scaleU", &QQuick3DTexture::setScaleU, transform.scale.x());
++ setProperty(*sceneTexture, "scaleV", &QQuick3DTexture::setScaleV, transform.scale.y());
++ }
++
++ // Image data: external file, or embedded in a buffer view or data: URI
++ const bool isEmbedded = image.bufferView >= 0 || QSSGGltfResourceResolver::isDataUri(image.uri);
++ if (!isEmbedded) {
++ QString relativePath = image.uri;
++ relativePath.replace(QLatin1Char('\\'), QLatin1Char('/'));
++ const QString resolved = QSSGGltfResourceResolver::resolveFilePath(relativePath, QString());
++ if (resolved.isEmpty()) {
++ qCWarning(lcQuick3DGltf) << "Ignoring image URI outside the asset directory:" << image.uri;
++ return sceneTexture;
++ }
++ const QString path = QDir(m_document->baseDir).absoluteFilePath(resolved);
++ setProperty(*sceneTexture, "source", &QQuick3DTexture::setSource, QUrl { path });
++ return sceneTexture;
++ }
++
++ QSSGSceneDesc::TextureData *textureData = m_textureDataMap.value(sourceImage, nullptr);
++ if (!textureData) {
++ QByteArray imageData;
++ QString mimeType = image.mimeType;
++ if (image.bufferView >= 0) {
++ const QSSGGltf::BufferView &view = m_document->bufferViews.at(image.bufferView);
++ imageData = m_document->buffers.at(view.buffer).data.mid(view.byteOffset, view.byteLength);
++ } else {
++ QString error;
++ imageData = QSSGGltfResourceResolver::decodeDataUri(image.uri, &error);
++ if (mimeType.isEmpty()) {
++ const qsizetype header = image.uri.indexOf(QLatin1Char(','));
++ // data:<mediatype>[;base64], drop any parameters
++ mimeType = image.uri.mid(5, header - 5).section(QLatin1Char(';'), 0, 0);
++ }
++ }
++ if (imageData.isEmpty()) {
++ qCWarning(lcQuick3DGltf) << "Failed to load embedded image" << sourceImage;
++ return sceneTexture;
++ }
++
++ // The format hint doubles as the file suffix
++ QByteArray format = mimeType.section(QLatin1Char('/'), 1, 1).trimmed().toLower().toLatin1();
++ if (format == QByteArrayLiteral("jpeg"))
++ format = QByteArrayLiteral("jpg");
++ else if (format.isEmpty())
++ format = QByteArrayLiteral("png");
++ textureData = new QSSGSceneDesc::TextureData(imageData, QSize(), format,
++ quint8(QSSGSceneDesc::TextureData::Flags::Compressed));
++ QSSGSceneDesc::addNode(*sceneTexture, *textureData);
++ m_textureDataMap.insert(sourceImage, textureData);
++ }
++ setProperty(*sceneTexture, "textureData", &QQuick3DTexture::setTextureData, textureData);
++
++ return sceneTexture;
++ }
++
++ void GltfSceneConverter::setLightProperties(QSSGSceneDesc::Light &target, const QSSGGltf::Light &source)
++ {
++ using QSSGSceneDesc::setProperty;
++
++ // Brightness is the largest component of color * intensity
++ const QVector3D scaledColor = source.color * source.intensity;
++ const float brightness = qMax(qMax(1.0f, scaledColor.x()), qMax(scaledColor.y(), scaledColor.z()));
++ const QColor color = QColor::fromRgbF(scaledColor.x() / brightness, scaledColor.y() / brightness,
++ scaledColor.z() / brightness);
++ setProperty(target, "color", &QQuick3DAbstractLight::setColor, color);
++ setProperty(target, "brightness", &QQuick3DAbstractLight::setBrightness, brightness);
++
++ // Inverse square falloff, scaled for Quick3D units
++ if (source.type == Light::Type::Point) {
++ setProperty(target, "linearFade", &QQuick3DPointLight::setLinearFade, 0.0f);
++ setProperty(target, "quadraticFade", &QQuick3DPointLight::setQuadraticFade, 10000.0f);
++ } else if (source.type == Light::Type::Spot) {
++ setProperty(target, "linearFade", &QQuick3DSpotLight::setLinearFade, 0.0f);
++ setProperty(target, "quadraticFade", &QQuick3DSpotLight::setQuadraticFade, 10000.0f);
++ // glTF cone angles are half angles from the center axis
++ setProperty(target, "coneAngle", &QQuick3DSpotLight::setConeAngle,
++ float(qRadiansToDegrees(source.outerConeAngle) * 2.0));
++ setProperty(target, "innerConeAngle", &QQuick3DSpotLight::setInnerConeAngle,
++ float(qRadiansToDegrees(source.innerConeAngle) * 2.0));
++ }
++ }
++
++ void GltfSceneConverter::setCameraProperties(QSSGSceneDesc::Camera &target, const QSSGGltf::Camera &source)
++ {
++ using QSSGSceneDesc::setProperty;
++
++ if (target.runtimeType == QSSGSceneDesc::Node::RuntimeType::PerspectiveCamera) {
++ setProperty(target, "clipNear", &QQuick3DPerspectiveCamera::setClipNear, source.znear);
++ // zfar 0 means infinite in glTF; pick a generous finite plane
++ const float clipFar = source.zfar > 0.0f ? source.zfar : 10000.0f;
++ setProperty(target, "clipFar", &QQuick3DPerspectiveCamera::setClipFar, clipFar);
++ setProperty(target, "fieldOfView", &QQuick3DPerspectiveCamera::setFieldOfView,
++ float(qRadiansToDegrees(source.yfov)));
++ } else {
++ setProperty(target, "clipNear", &QQuick3DOrthographicCamera::setClipNear, source.znear);
++ // Required by the specification, but not enforced by the parser
++ const float clipFar = source.zfar > 0.0f ? source.zfar : 10000.0f;
++ setProperty(target, "clipFar", &QQuick3DOrthographicCamera::setClipFar, clipFar);
++ // xmag/ymag are half the orthographic view size in scene units
++ setProperty(target, "horizontalMagnification", &QQuick3DOrthographicCamera::setHorizontalMagnification,
++ source.xmag * 2.0f);
++ setProperty(target, "verticalMagnification", &QQuick3DOrthographicCamera::setVerticalMagnification,
++ source.ymag * 2.0f);
++ }
++ }
++
++ QT_END_NAMESPACE