337bool QSSGGltfParser::parse(
const QByteArray &data,
const QString &baseDir, QSSGGltfDocument *document)
340 m_errorMessage.clear();
341 *document = QSSGGltfDocument();
342 document->baseDir = baseDir;
344 QByteArray json = data;
348 if (data.size() >= 12 && qFromLittleEndian<quint32>(data.constData()) == GLB_MAGIC) {
349 const quint32 version = qFromLittleEndian<quint32>(data.constData() + 4);
351 return setError(QStringLiteral(
"Unsupported GLB container version %1").arg(version));
352 const quint32 length = qFromLittleEndian<quint32>(data.constData() + 8);
353 if (qint64(length) > data.size())
354 return setError(QStringLiteral(
"Truncated GLB file: header declares %1 bytes, got %2")
355 .arg(length).arg(data.size()));
359 while (offset + 8 <= qint64(length)) {
360 const quint32 chunkLength = qFromLittleEndian<quint32>(data.constData() + offset);
361 const quint32 chunkType = qFromLittleEndian<quint32>(data.constData() + offset + 4);
363 if (offset + qint64(chunkLength) > qint64(length))
364 return setError(QStringLiteral(
"Truncated GLB chunk at offset %1").arg(offset - 8));
366 if (chunkType == GLB_CHUNK_JSON && json.isEmpty())
367 json = data.mid(offset, chunkLength);
368 else if (chunkType == GLB_CHUNK_BIN && binChunk.isEmpty())
369 binChunk = data.mid(offset, chunkLength);
371 qCWarning(lcQuick3DGltf) <<
"Skipping unknown GLB chunk type" << Qt::hex << chunkType;
374 offset += chunkLength;
376 offset += 4 - (offset % 4);
379 return setError(QStringLiteral(
"GLB container has no JSON chunk"));
382 QJsonParseError jsonError;
383 const QJsonDocument jsonDocument = QJsonDocument::fromJson(json, &jsonError);
384 if (jsonDocument.isNull())
385 return setError(QStringLiteral(
"Invalid glTF JSON: %1").arg(jsonError.errorString()));
386 if (!jsonDocument.isObject())
387 return setError(QStringLiteral(
"Invalid glTF JSON: root is not an object"));
389 const QJsonObject root = jsonDocument.object();
393 const QJsonValue assetValue = root.value(QLatin1String(
"asset"));
394 if (!assetValue.isObject())
395 return setError(QStringLiteral(
"Not a glTF document: no asset object"));
396 const QJsonObject asset = assetValue.toObject();
397 document->asset.version = asset.value(QLatin1String(
"version")).toString();
398 document->asset.minVersion = asset.value(QLatin1String(
"minVersion")).toString();
399 document->asset.generator = asset.value(QLatin1String(
"generator")).toString();
400 document->asset.copyright = asset.value(QLatin1String(
"copyright")).toString();
402 const int major = document->asset.version.section(QLatin1Char(
'.'), 0, 0).toInt();
404 return setError(QStringLiteral(
"Unsupported glTF version '%1'").arg(document->asset.version));
409 if (!document->asset.minVersion.isEmpty()) {
410 const int minMajor = document->asset.minVersion.section(QLatin1Char(
'.'), 0, 0).toInt();
411 const int minMinor = document->asset.minVersion.section(QLatin1Char(
'.'), 1, 1).toInt();
412 if (minMajor != 2 || minMinor > 0)
413 return setError(QStringLiteral(
"Asset requires glTF version '%1' or higher")
414 .arg(document->asset.minVersion));
419 const QJsonArray extensionsUsed = root.value(QLatin1String(
"extensionsUsed")).toArray();
420 for (
const auto &value : extensionsUsed)
421 document->extensionsUsed.append(value.toString());
422 const QJsonArray extensionsRequired = root.value(QLatin1String(
"extensionsRequired")).toArray();
423 for (
const auto &value : extensionsRequired)
424 document->extensionsRequired.append(value.toString());
425 document->rootExtensions = root.value(QLatin1String(
"extensions")).toObject();
429 const QJsonValue variantsExt = document->rootExtensions.value(QLatin1String(
"KHR_materials_variants"));
430 if (variantsExt.isObject()) {
431 const QJsonArray variants = variantsExt.toObject().value(QLatin1String(
"variants")).toArray();
432 for (
const auto &value : variants)
433 document->materialVariants.append(value.toObject().value(QLatin1String(
"name")).toString());
437 const QStringList supported = supportedExtensions();
438 for (
const QString &required : std::as_const(document->extensionsRequired)) {
439 if (!supported.contains(required))
440 return setError(QStringLiteral(
"Asset requires unsupported glTF extension '%1'").arg(required));
442 for (
const QString &used : std::as_const(document->extensionsUsed)) {
443 if (!supported.contains(used))
444 qCWarning(lcQuick3DGltf) <<
"Ignoring unsupported glTF extension" << used;
448 const QJsonArray buffers = root.value(QLatin1String(
"buffers")).toArray();
449 for (
const auto &value : buffers) {
450 const QJsonObject object = value.toObject();
452 buffer.uri = object.value(QLatin1String(
"uri")).toString();
453 buffer.byteLength = qint64(object.value(QLatin1String(
"byteLength")).toDouble());
454 if (buffer.byteLength < 0)
455 return setError(QStringLiteral(
"Buffer %1 has negative byte length").arg(document->buffers.size()));
456 buffer.meshoptFallback = object.value(QLatin1String(
"extensions")).toObject()
457 .value(QLatin1String(
"EXT_meshopt_compression")).toObject()
458 .value(QLatin1String(
"fallback")).toBool(
false);
459 if (buffer.meshoptFallback) {
460 document->buffers.append(buffer);
463 if (buffer.uri.isEmpty()) {
465 if (document->buffers.isEmpty() && !binChunk.isEmpty())
466 buffer.data = binChunk;
467 else if (binChunk.isEmpty())
469 QStringLiteral(
"Buffer %1 has no URI and there is no GLB BIN chunk")
470 .arg(document->buffers.size()));
472 return setError(QStringLiteral(
"Only the first buffer may refer to the GLB BIN chunk"));
474 QString resolveError;
475 buffer.data = QSSGGltfResourceResolver::loadUri(buffer.uri, baseDir, &resolveError);
479 if (!resolveError.isEmpty())
480 return setError(QStringLiteral(
"Failed to load buffer %1: %2")
481 .arg(document->buffers.size()).arg(resolveError));
483 if (buffer.data.size() < buffer.byteLength)
484 return setError(QStringLiteral(
"Buffer %1 is %2 bytes, expected at least %3")
485 .arg(document->buffers.size()).arg(buffer.data.size()).arg(buffer.byteLength));
486 document->buffers.append(buffer);
490 MeshoptDecodeState meshoptState;
491 const QJsonArray bufferViews = root.value(QLatin1String(
"bufferViews")).toArray();
492 for (
const auto &value : bufferViews) {
493 const QJsonObject object = value.toObject();
495 view.buffer = object.value(QLatin1String(
"buffer")).toInt(-1);
496 view.byteOffset = qint64(object.value(QLatin1String(
"byteOffset")).toDouble(0));
497 view.byteLength = qint64(object.value(QLatin1String(
"byteLength")).toDouble());
498 view.byteStride = object.value(QLatin1String(
"byteStride")).toInt(0);
499 view.target = object.value(QLatin1String(
"target")).toInt(0);
500 view.name = object.value(QLatin1String(
"name")).toString();
502 const QJsonValue meshoptValue = object.value(QLatin1String(
"extensions")).toObject()
503 .value(QLatin1String(
"EXT_meshopt_compression"));
504 if (meshoptValue.isObject()) {
506 if (!decodeMeshoptBufferView(meshoptValue.toObject(), *document, view, meshoptState, &decodeError)) {
507 return setError(QStringLiteral(
"Buffer view %1: EXT_meshopt_compression: %2")
508 .arg(document->bufferViews.size()).arg(decodeError));
510 document->bufferViews.append(view);
514 if (view.buffer < 0 || view.buffer >= document->buffers.size())
516 QStringLiteral(
"Buffer view %1 references invalid buffer %2")
517 .arg(document->bufferViews.size()).arg(view.buffer));
518 if (document->buffers.at(view.buffer).meshoptFallback)
519 return setError(QStringLiteral(
"Buffer view %1 references unloaded fallback buffer %2")
520 .arg(document->bufferViews.size()).arg(view.buffer));
524 if (view.byteOffset < 0 || view.byteLength < 0 || view.byteStride < 0 || view.byteStride > 252)
525 return setError(QStringLiteral(
"Buffer view %1 has an invalid byte offset, length, or stride")
526 .arg(document->bufferViews.size()));
530 const qint64 bufferLength = document->buffers.at(view.buffer).byteLength;
531 if (view.byteLength > bufferLength || view.byteOffset > bufferLength - view.byteLength)
533 QStringLiteral(
"Buffer view %1 extends past the end of buffer %2")
534 .arg(document->bufferViews.size()).arg(view.buffer));
535 document->bufferViews.append(view);
539 const QJsonArray accessors = root.value(QLatin1String(
"accessors")).toArray();
540 for (
const auto &value : accessors) {
541 const QJsonObject object = value.toObject();
543 accessor.bufferView = object.value(QLatin1String(
"bufferView")).toInt(-1);
544 accessor.byteOffset = qint64(object.value(QLatin1String(
"byteOffset")).toDouble(0));
545 const int componentType = object.value(QLatin1String(
"componentType")).toInt();
546 if (!isValidComponentType(componentType))
547 return setError(QStringLiteral(
"Accessor %1 has invalid component type %2")
548 .arg(document->accessors.size()).arg(componentType));
549 accessor.componentType = Accessor::ComponentType(componentType);
551 accessor.type = accessorTypeFromString(object.value(QLatin1String(
"type")).toString(), &typeOk);
553 return setError(QStringLiteral(
"Accessor %1 has invalid type '%2'")
554 .arg(document->accessors.size())
555 .arg(object.value(QLatin1String(
"type")).toString()));
564 constexpr qint64 maxAccessorElements = qint64(256) * 1024 * 1024;
565 const double rawCount = object.value(QLatin1String(
"count")).toDouble();
566 if (!(rawCount >= 0.0 && rawCount <=
double(maxAccessorElements)))
567 return setError(QStringLiteral(
"Accessor %1 has a negative or unreasonably large count")
568 .arg(document->accessors.size()));
569 accessor.count = qint64(rawCount);
570 if (accessor.byteOffset < 0)
571 return setError(QStringLiteral(
"Accessor %1 has a negative byte offset")
572 .arg(document->accessors.size()));
573 accessor.normalized = object.value(QLatin1String(
"normalized")).toBool(
false);
574 accessor.name = object.value(QLatin1String(
"name")).toString();
575 const QJsonArray minValues = object.value(QLatin1String(
"min")).toArray();
576 for (
const auto &m : minValues)
577 accessor.min.append(m.toDouble());
578 const QJsonArray maxValues = object.value(QLatin1String(
"max")).toArray();
579 for (
const auto &m : maxValues)
580 accessor.max.append(m.toDouble());
582 const QJsonValue sparseValue = object.value(QLatin1String(
"sparse"));
583 if (sparseValue.isObject()) {
584 const QJsonObject sparseObject = sparseValue.toObject();
585 Accessor::Sparse sparse;
588 const double rawSparseCount = sparseObject.value(QLatin1String(
"count")).toDouble();
589 if (!(rawSparseCount >= 0.0 && rawSparseCount <=
double(maxAccessorElements)))
590 return setError(QStringLiteral(
"Accessor %1 has a negative or unreasonably large sparse count")
591 .arg(document->accessors.size()));
592 sparse.count = qint64(rawSparseCount);
593 const QJsonObject indices = sparseObject.value(QLatin1String(
"indices")).toObject();
594 sparse.indicesBufferView = indices.value(QLatin1String(
"bufferView")).toInt(-1);
595 sparse.indicesByteOffset = qint64(indices.value(QLatin1String(
"byteOffset")).toDouble(0));
596 const int indicesComponentType = indices.value(QLatin1String(
"componentType")).toInt();
597 if (!isValidComponentType(indicesComponentType))
599 QStringLiteral(
"Accessor %1 sparse indices have invalid component type")
600 .arg(document->accessors.size()));
601 sparse.indicesComponentType = Accessor::ComponentType(indicesComponentType);
602 const QJsonObject values = sparseObject.value(QLatin1String(
"values")).toObject();
603 sparse.valuesBufferView = values.value(QLatin1String(
"bufferView")).toInt(-1);
604 sparse.valuesByteOffset = qint64(values.value(QLatin1String(
"byteOffset")).toDouble(0));
606 if (sparse.indicesBufferView < 0 || sparse.indicesBufferView >= document->bufferViews.size()
607 || sparse.valuesBufferView < 0 || sparse.valuesBufferView >= document->bufferViews.size()) {
608 return setError(QStringLiteral(
"Accessor %1 sparse data references an invalid buffer view")
609 .arg(document->accessors.size()));
611 if (sparse.count > accessor.count
612 || sparse.indicesByteOffset < 0 || sparse.valuesByteOffset < 0) {
613 return setError(QStringLiteral(
"Accessor %1 has an invalid sparse count or byte offset")
614 .arg(document->accessors.size()));
617 const BufferView &indicesView = document->bufferViews.at(sparse.indicesBufferView);
618 const BufferView &valuesView = document->bufferViews.at(sparse.valuesBufferView);
619 const qint64 indicesSpan =
620 sparse.count * Accessor::componentByteSize(sparse.indicesComponentType);
621 const qint64 valuesSpan = sparse.count * accessor.elementByteSize();
622 if (indicesSpan > indicesView.byteLength
623 || sparse.indicesByteOffset > indicesView.byteLength - indicesSpan
624 || valuesSpan > valuesView.byteLength
625 || sparse.valuesByteOffset > valuesView.byteLength - valuesSpan) {
627 QStringLiteral(
"Accessor %1 sparse data extends past the end of its buffer view")
628 .arg(document->accessors.size()));
630 accessor.sparse = sparse;
633 if (accessor.bufferView >= document->bufferViews.size())
635 QStringLiteral(
"Accessor %1 references invalid buffer view %2")
636 .arg(document->accessors.size()).arg(accessor.bufferView));
637 if (accessor.bufferView >= 0) {
638 const BufferView &view = document->bufferViews.at(accessor.bufferView);
639 const qint64 elementSize = accessor.elementByteSize();
640 const qint64 stride = view.byteStride > 0 ? view.byteStride : elementSize;
642 const qint64 span = (accessor.count - 1) * stride + elementSize;
643 if (accessor.count > 0
644 && (span > view.byteLength || accessor.byteOffset > view.byteLength - span))
645 return setError(QStringLiteral(
"Accessor %1 extends past the end of buffer view %2")
646 .arg(document->accessors.size())
647 .arg(accessor.bufferView));
653 constexpr qint64 maxSyntheticAccessorBytes = qint64(256) * 1024 * 1024;
654 if (accessor.count * accessor.elementByteSize() > maxSyntheticAccessorBytes)
655 return setError(QStringLiteral(
"Accessor %1 has no buffer view and an unreasonably large count")
656 .arg(document->accessors.size()));
658 document->accessors.append(accessor);
662 const QJsonArray images = root.value(QLatin1String(
"images")).toArray();
663 for (
const auto &value : images) {
664 const QJsonObject object = value.toObject();
666 image.uri = object.value(QLatin1String(
"uri")).toString();
667 image.bufferView = object.value(QLatin1String(
"bufferView")).toInt(-1);
668 image.mimeType = object.value(QLatin1String(
"mimeType")).toString();
669 image.name = object.value(QLatin1String(
"name")).toString();
670 if (image.bufferView >= document->bufferViews.size())
671 return setError(QStringLiteral(
"Image %1 references invalid buffer view %2")
672 .arg(document->images.size()).arg(image.bufferView));
673 document->images.append(image);
677 const QJsonArray samplers = root.value(QLatin1String(
"samplers")).toArray();
678 for (
const auto &value : samplers) {
679 const QJsonObject object = value.toObject();
681 sampler.magFilter = object.value(QLatin1String(
"magFilter")).toInt(0);
682 sampler.minFilter = object.value(QLatin1String(
"minFilter")).toInt(0);
683 sampler.wrapS = object.value(QLatin1String(
"wrapS")).toInt(Sampler::Repeat);
684 sampler.wrapT = object.value(QLatin1String(
"wrapT")).toInt(Sampler::Repeat);
685 sampler.name = object.value(QLatin1String(
"name")).toString();
686 document->samplers.append(sampler);
690 const QJsonArray textures = root.value(QLatin1String(
"textures")).toArray();
691 for (
const auto &value : textures) {
692 const QJsonObject object = value.toObject();
694 texture.sampler = object.value(QLatin1String(
"sampler")).toInt(-1);
695 texture.source = object.value(QLatin1String(
"source")).toInt(-1);
696 texture.name = object.value(QLatin1String(
"name")).toString();
697 texture.extensions = object.value(QLatin1String(
"extensions")).toObject();
699 const QJsonValue webpExt = texture.extensions.value(QLatin1String(
"EXT_texture_webp"));
700 if (webpExt.isObject())
701 texture.webpSource = webpExt.toObject().value(QLatin1String(
"source")).toInt(-1);
703 if (texture.sampler >= document->samplers.size())
704 return setError(QStringLiteral(
"Texture %1 references invalid sampler %2")
705 .arg(document->textures.size()).arg(texture.sampler));
706 if (texture.source >= document->images.size() || texture.webpSource >= document->images.size())
707 return setError(QStringLiteral(
"Texture %1 references invalid image %2")
708 .arg(document->textures.size())
709 .arg(qMax(texture.source, texture.webpSource)));
710 document->textures.append(texture);
719 int badTextureIndex = -1;
720 const auto parseTexture = [&](
const QJsonObject &object,
721 const char *scaleOrStrengthKey =
nullptr) {
722 const TextureInfo info = parseTextureInfo(object, scaleOrStrengthKey);
723 if (info.index >= document->textures.size())
724 badTextureIndex = info.index;
728 const QJsonArray materials = root.value(QLatin1String(
"materials")).toArray();
729 for (
const auto &value : materials) {
730 const QJsonObject object = value.toObject();
732 material.name = object.value(QLatin1String(
"name")).toString();
734 const QJsonValue pbrValue = object.value(QLatin1String(
"pbrMetallicRoughness"));
735 if (pbrValue.isObject()) {
736 const QJsonObject pbr = pbrValue.toObject();
737 material.baseColorFactor =
738 toVector4D(pbr.value(QLatin1String(
"baseColorFactor")).toArray(), material.baseColorFactor);
739 material.baseColorTexture = parseTexture(pbr.value(QLatin1String(
"baseColorTexture")).toObject());
740 material.metallicFactor =
float(pbr.value(QLatin1String(
"metallicFactor")).toDouble(1.0));
741 material.roughnessFactor =
float(pbr.value(QLatin1String(
"roughnessFactor")).toDouble(1.0));
742 material.metallicRoughnessTexture =
743 parseTexture(pbr.value(QLatin1String(
"metallicRoughnessTexture")).toObject());
746 material.normalTexture = parseTexture(object.value(QLatin1String(
"normalTexture")).toObject(),
"scale");
747 material.occlusionTexture =
748 parseTexture(object.value(QLatin1String(
"occlusionTexture")).toObject(),
"strength");
749 material.emissiveTexture = parseTexture(object.value(QLatin1String(
"emissiveTexture")).toObject());
750 material.emissiveFactor =
751 toVector3D(object.value(QLatin1String(
"emissiveFactor")).toArray(), material.emissiveFactor);
753 const QString alphaMode = object.value(QLatin1String(
"alphaMode")).toString();
754 if (alphaMode == QLatin1String(
"MASK"))
755 material.alphaMode = Material::AlphaMode::Mask;
756 else if (alphaMode == QLatin1String(
"BLEND"))
757 material.alphaMode = Material::AlphaMode::Blend;
758 material.alphaCutoff =
float(object.value(QLatin1String(
"alphaCutoff")).toDouble(0.5));
759 material.doubleSided = object.value(QLatin1String(
"doubleSided")).toBool(
false);
761 material.extensions = object.value(QLatin1String(
"extensions")).toObject();
762 const QJsonObject &ext = material.extensions;
764 material.unlit = ext.contains(QLatin1String(
"KHR_materials_unlit"));
766 const QJsonValue sgValue = ext.value(QLatin1String(
"KHR_materials_pbrSpecularGlossiness"));
767 if (sgValue.isObject()) {
768 const QJsonObject sg = sgValue.toObject();
769 Material::SpecularGlossiness specularGlossiness;
770 specularGlossiness.diffuseFactor = toVector4D(sg.value(QLatin1String(
"diffuseFactor")).toArray(),
771 specularGlossiness.diffuseFactor);
772 specularGlossiness.diffuseTexture = parseTexture(sg.value(QLatin1String(
"diffuseTexture")).toObject());
773 specularGlossiness.specularFactor = toVector3D(sg.value(QLatin1String(
"specularFactor")).toArray(),
774 specularGlossiness.specularFactor);
775 specularGlossiness.glossinessFactor =
float(sg.value(QLatin1String(
"glossinessFactor")).toDouble(1.0));
776 specularGlossiness.specularGlossinessTexture = parseTexture(
777 sg.value(QLatin1String(
"specularGlossinessTexture")).toObject());
778 material.specularGlossiness = specularGlossiness;
781 const QJsonValue ccValue = ext.value(QLatin1String(
"KHR_materials_clearcoat"));
782 if (ccValue.isObject()) {
783 const QJsonObject cc = ccValue.toObject();
784 Material::Clearcoat clearcoat;
785 clearcoat.clearcoatFactor =
float(cc.value(QLatin1String(
"clearcoatFactor")).toDouble(0.0));
786 clearcoat.clearcoatTexture = parseTexture(cc.value(QLatin1String(
"clearcoatTexture")).toObject());
787 clearcoat.clearcoatRoughnessFactor =
788 float(cc.value(QLatin1String(
"clearcoatRoughnessFactor")).toDouble(0.0));
789 clearcoat.clearcoatRoughnessTexture =
790 parseTexture(cc.value(QLatin1String(
"clearcoatRoughnessTexture")).toObject());
791 clearcoat.clearcoatNormalTexture =
792 parseTexture(cc.value(QLatin1String(
"clearcoatNormalTexture")).toObject(),
"scale");
793 material.clearcoat = clearcoat;
796 const QJsonValue trValue = ext.value(QLatin1String(
"KHR_materials_transmission"));
797 if (trValue.isObject()) {
798 const QJsonObject tr = trValue.toObject();
799 Material::Transmission transmission;
800 transmission.transmissionFactor =
float(tr.value(QLatin1String(
"transmissionFactor")).toDouble(0.0));
801 transmission.transmissionTexture =
802 parseTexture(tr.value(QLatin1String(
"transmissionTexture")).toObject());
803 material.transmission = transmission;
806 const QJsonValue volValue = ext.value(QLatin1String(
"KHR_materials_volume"));
807 if (volValue.isObject()) {
808 const QJsonObject vol = volValue.toObject();
809 Material::Volume volume;
810 volume.thicknessFactor =
float(vol.value(QLatin1String(
"thicknessFactor")).toDouble(0.0));
811 volume.thicknessTexture = parseTexture(vol.value(QLatin1String(
"thicknessTexture")).toObject());
812 volume.attenuationDistance =
float(vol.value(QLatin1String(
"attenuationDistance")).toDouble(0.0));
813 volume.attenuationColor =
814 toVector3D(vol.value(QLatin1String(
"attenuationColor")).toArray(), volume.attenuationColor);
815 material.volume = volume;
818 const QJsonValue iorValue = ext.value(QLatin1String(
"KHR_materials_ior"));
819 if (iorValue.isObject())
820 material.ior =
float(iorValue.toObject().value(QLatin1String(
"ior")).toDouble(1.5));
822 const QJsonValue esValue = ext.value(QLatin1String(
"KHR_materials_emissive_strength"));
823 if (esValue.isObject())
824 material.emissiveStrength =
825 float(esValue.toObject().value(QLatin1String(
"emissiveStrength")).toDouble(1.0));
827 const QJsonValue spValue = ext.value(QLatin1String(
"KHR_materials_specular"));
828 if (spValue.isObject()) {
829 const QJsonObject sp = spValue.toObject();
830 Material::Specular specular;
831 specular.specularFactor =
float(sp.value(QLatin1String(
"specularFactor")).toDouble(1.0));
832 specular.specularTexture = parseTexture(sp.value(QLatin1String(
"specularTexture")).toObject());
833 specular.specularColorFactor = toVector3D(sp.value(QLatin1String(
"specularColorFactor")).toArray(),
834 specular.specularColorFactor);
835 specular.specularColorTexture =
836 parseTexture(sp.value(QLatin1String(
"specularColorTexture")).toObject());
837 material.specular = specular;
840 const QJsonValue shValue = ext.value(QLatin1String(
"KHR_materials_sheen"));
841 if (shValue.isObject()) {
842 const QJsonObject sh = shValue.toObject();
843 Material::Sheen sheen;
844 sheen.sheenColorFactor = toVector3D(sh.value(QLatin1String(
"sheenColorFactor")).toArray(), sheen.sheenColorFactor);
845 sheen.sheenColorTexture = parseTexture(sh.value(QLatin1String(
"sheenColorTexture")).toObject());
846 sheen.sheenRoughnessFactor =
float(sh.value(QLatin1String(
"sheenRoughnessFactor")).toDouble(0.0));
847 sheen.sheenRoughnessTexture = parseTexture(sh.value(QLatin1String(
"sheenRoughnessTexture")).toObject());
848 material.sheen = sheen;
851 const QJsonValue anValue = ext.value(QLatin1String(
"KHR_materials_anisotropy"));
852 if (anValue.isObject()) {
853 const QJsonObject an = anValue.toObject();
854 Material::Anisotropy anisotropy;
855 anisotropy.anisotropyStrength =
float(an.value(QLatin1String(
"anisotropyStrength")).toDouble(0.0));
856 anisotropy.anisotropyRotation =
float(an.value(QLatin1String(
"anisotropyRotation")).toDouble(0.0));
857 anisotropy.anisotropyTexture = parseTexture(an.value(QLatin1String(
"anisotropyTexture")).toObject());
858 material.anisotropy = anisotropy;
861 const QJsonValue irValue = ext.value(QLatin1String(
"KHR_materials_iridescence"));
862 if (irValue.isObject()) {
863 const QJsonObject ir = irValue.toObject();
864 Material::Iridescence iridescence;
865 iridescence.iridescenceFactor =
float(ir.value(QLatin1String(
"iridescenceFactor")).toDouble(0.0));
866 iridescence.iridescenceTexture = parseTexture(ir.value(QLatin1String(
"iridescenceTexture")).toObject());
867 iridescence.iridescenceIor =
float(ir.value(QLatin1String(
"iridescenceIor")).toDouble(1.3));
868 iridescence.iridescenceThicknessMinimum =
float(ir.value(QLatin1String(
"iridescenceThicknessMinimum")).toDouble(100.0));
869 iridescence.iridescenceThicknessMaximum =
float(ir.value(QLatin1String(
"iridescenceThicknessMaximum")).toDouble(400.0));
870 iridescence.iridescenceThicknessTexture = parseTexture(
871 ir.value(QLatin1String(
"iridescenceThicknessTexture")).toObject());
872 material.iridescence = iridescence;
875 const QJsonValue dispValue = ext.value(QLatin1String(
"KHR_materials_dispersion"));
876 if (dispValue.isObject())
877 material.dispersion =
float(dispValue.toObject().value(QLatin1String(
"dispersion")).toDouble(0.0));
879 document->materials.append(material);
882 if (badTextureIndex >= 0)
883 return setError(QStringLiteral(
"Material references invalid texture %1").arg(badTextureIndex));
886 const QJsonArray meshes = root.value(QLatin1String(
"meshes")).toArray();
887 for (
const auto &value : meshes) {
888 const QJsonObject object = value.toObject();
890 mesh.name = object.value(QLatin1String(
"name")).toString();
891 const QJsonArray weights = object.value(QLatin1String(
"weights")).toArray();
892 for (
const auto &w : weights)
893 mesh.weights.append(
float(w.toDouble()));
895 const QJsonArray primitives = object.value(QLatin1String(
"primitives")).toArray();
896 for (
const auto &primitiveValue : primitives) {
897 const QJsonObject primitiveObject = primitiveValue.toObject();
898 MeshPrimitive primitive;
899 const QJsonObject attributes = primitiveObject.value(QLatin1String(
"attributes")).toObject();
900 for (
auto it = attributes.constBegin(); it != attributes.constEnd(); ++it)
901 primitive.attributes.insert(it.key().toUtf8(), it.value().toInt(-1));
902 primitive.indices = primitiveObject.value(QLatin1String(
"indices")).toInt(-1);
903 primitive.material = primitiveObject.value(QLatin1String(
"material")).toInt(-1);
904 primitive.mode = primitiveObject.value(QLatin1String(
"mode")).toInt(MeshPrimitive::Triangles);
905 const QJsonArray targets = primitiveObject.value(QLatin1String(
"targets")).toArray();
906 for (
const auto &targetValue : targets) {
907 const QJsonObject targetObject = targetValue.toObject();
908 QHash<QByteArray,
int> target;
909 for (
auto it = targetObject.constBegin(); it != targetObject.constEnd(); ++it)
910 target.insert(it.key().toUtf8(), it.value().toInt(-1));
911 primitive.targets.append(target);
913 primitive.extensions = primitiveObject.value(QLatin1String(
"extensions")).toObject();
915 const QJsonValue variantsExt = primitive.extensions.value(QLatin1String(
"KHR_materials_variants"));
916 if (variantsExt.isObject()) {
917 const QJsonArray mappings = variantsExt.toObject().value(QLatin1String(
"mappings")).toArray();
918 for (
const auto &mappingValue : mappings) {
919 const QJsonObject mappingObject = mappingValue.toObject();
920 MeshPrimitive::VariantMapping mapping;
921 mapping.material = mappingObject.value(QLatin1String(
"material")).toInt(-1);
922 if (mapping.material < 0 || mapping.material >= document->materials.size()) {
924 QStringLiteral(
"Mesh %1 primitive variant mapping references invalid material %2")
925 .arg(document->meshes.size())
926 .arg(mapping.material));
928 const QJsonArray mappingVariants = mappingObject.value(QLatin1String(
"variants")).toArray();
929 for (
const auto &variantValue : mappingVariants) {
930 const int variant = variantValue.toInt(-1);
931 if (variant < 0 || variant >= document->materialVariants.size()) {
933 QStringLiteral(
"Mesh %1 primitive variant mapping references invalid variant %2")
934 .arg(document->meshes.size())
937 mapping.variants.append(variant);
939 primitive.variantMappings.append(mapping);
947 const auto checkAccessor = [&](
int accessor,
const char *what) {
948 if (accessor >= document->accessors.size())
949 return setError(QStringLiteral(
"Mesh %1 primitive references invalid %2 accessor %3")
950 .arg(document->meshes.size()).arg(QLatin1String(what)).arg(accessor));
953 for (
auto it = primitive.attributes.constBegin(); it != primitive.attributes.constEnd(); ++it) {
954 if (!checkAccessor(it.value(), it.key().constData()))
957 if (primitive.indices >= 0 && !checkAccessor(primitive.indices,
"index"))
959 if (primitive.material >= document->materials.size())
960 return setError(QStringLiteral(
"Mesh %1 primitive references invalid material %2")
961 .arg(document->meshes.size())
962 .arg(primitive.material));
964 mesh.primitives.append(primitive);
966 if (mesh.primitives.isEmpty())
967 qCWarning(lcQuick3DGltf) <<
"Mesh" << document->meshes.size() <<
"has no primitives";
968 document->meshes.append(mesh);
972 const QJsonArray cameras = root.value(QLatin1String(
"cameras")).toArray();
973 for (
const auto &value : cameras) {
974 const QJsonObject object = value.toObject();
976 camera.name = object.value(QLatin1String(
"name")).toString();
977 const QString type = object.value(QLatin1String(
"type")).toString();
978 if (type == QLatin1String(
"orthographic")) {
979 camera.type = Camera::Type::Orthographic;
980 const QJsonObject ortho = object.value(QLatin1String(
"orthographic")).toObject();
981 camera.xmag =
float(ortho.value(QLatin1String(
"xmag")).toDouble());
982 camera.ymag =
float(ortho.value(QLatin1String(
"ymag")).toDouble());
983 camera.znear =
float(ortho.value(QLatin1String(
"znear")).toDouble());
984 camera.zfar =
float(ortho.value(QLatin1String(
"zfar")).toDouble());
986 camera.type = Camera::Type::Perspective;
987 const QJsonObject perspective = object.value(QLatin1String(
"perspective")).toObject();
988 camera.aspectRatio =
float(perspective.value(QLatin1String(
"aspectRatio")).toDouble(0.0));
989 camera.yfov =
float(perspective.value(QLatin1String(
"yfov")).toDouble());
990 camera.znear =
float(perspective.value(QLatin1String(
"znear")).toDouble());
991 camera.zfar =
float(perspective.value(QLatin1String(
"zfar")).toDouble(0.0));
993 document->cameras.append(camera);
998 const QJsonValue lightsExt = document->rootExtensions.value(QLatin1String(
"KHR_lights_punctual"));
999 if (lightsExt.isObject()) {
1000 const QJsonArray lights = lightsExt.toObject().value(QLatin1String(
"lights")).toArray();
1001 for (
const auto &value : lights) {
1002 const QJsonObject object = value.toObject();
1004 light.name = object.value(QLatin1String(
"name")).toString();
1005 const QString type = object.value(QLatin1String(
"type")).toString();
1006 if (type == QLatin1String(
"point"))
1007 light.type = Light::Type::Point;
1008 else if (type == QLatin1String(
"spot"))
1009 light.type = Light::Type::Spot;
1011 light.type = Light::Type::Directional;
1012 light.color = toVector3D(object.value(QLatin1String(
"color")).toArray(), light.color);
1013 light.intensity =
float(object.value(QLatin1String(
"intensity")).toDouble(1.0));
1014 light.range =
float(object.value(QLatin1String(
"range")).toDouble(0.0));
1015 if (light.type == Light::Type::Spot) {
1016 const QJsonObject spot = object.value(QLatin1String(
"spot")).toObject();
1017 light.innerConeAngle =
float(spot.value(QLatin1String(
"innerConeAngle")).toDouble(0.0));
1018 light.outerConeAngle =
float(spot.value(QLatin1String(
"outerConeAngle")).toDouble(M_PI_4));
1020 document->lights.append(light);
1026 const QJsonArray nodes = root.value(QLatin1String(
"nodes")).toArray();
1027 for (
const auto &value : nodes) {
1028 const QJsonObject object = value.toObject();
1030 node.name = object.value(QLatin1String(
"name")).toString();
1031 const QJsonArray children = object.value(QLatin1String(
"children")).toArray();
1032 for (
const auto &child : children)
1033 node.children.append(child.toInt(-1));
1034 node.mesh = object.value(QLatin1String(
"mesh")).toInt(-1);
1035 node.skin = object.value(QLatin1String(
"skin")).toInt(-1);
1036 node.camera = object.value(QLatin1String(
"camera")).toInt(-1);
1038 const QJsonValue matrixValue = object.value(QLatin1String(
"matrix"));
1039 if (matrixValue.isArray()) {
1040 const QJsonArray m = matrixValue.toArray();
1041 if (m.size() == 16) {
1043 for (
int i = 0; i < 16; ++i)
1044 values[i] =
float(m.at(i).toDouble());
1046 node.matrix = QMatrix4x4(values).transposed();
1047 node.hasMatrix =
true;
1050 node.translation = toVector3D(object.value(QLatin1String(
"translation")).toArray(), node.translation);
1051 const QJsonArray rotation = object.value(QLatin1String(
"rotation")).toArray();
1052 if (rotation.size() == 4) {
1054 node.rotation = QQuaternion(
float(rotation.at(3).toDouble()),
float(rotation.at(0).toDouble()),
1055 float(rotation.at(1).toDouble()),
float(rotation.at(2).toDouble()));
1057 node.scale = toVector3D(object.value(QLatin1String(
"scale")).toArray(), node.scale);
1058 const QJsonArray weights = object.value(QLatin1String(
"weights")).toArray();
1059 for (
const auto &w : weights)
1060 node.weights.append(
float(w.toDouble()));
1061 node.extensions = object.value(QLatin1String(
"extensions")).toObject();
1063 const QJsonValue lightExt = node.extensions.value(QLatin1String(
"KHR_lights_punctual"));
1064 if (lightExt.isObject())
1065 node.light = lightExt.toObject().value(QLatin1String(
"light")).toInt(-1);
1067 const QJsonValue visibilityExt = node.extensions.value(QLatin1String(
"KHR_node_visibility"));
1068 if (visibilityExt.isObject())
1069 node.visible = visibilityExt.toObject().value(QLatin1String(
"visible")).toBool(
true);
1071 const QJsonValue instancingExt = node.extensions.value(QLatin1String(
"EXT_mesh_gpu_instancing"));
1072 if (instancingExt.isObject()) {
1073 const QJsonObject instanceAttributes =
1074 instancingExt.toObject().value(QLatin1String(
"attributes")).toObject();
1075 for (
auto it = instanceAttributes.constBegin(); it != instanceAttributes.constEnd(); ++it) {
1076 const int accessor = it.value().toInt(-1);
1077 if (accessor < 0 || accessor >= document->accessors.size()) {
1078 return setError(QStringLiteral(
"Node %1 instance attribute %2 references invalid accessor %3")
1079 .arg(document->nodes.size()).arg(it.key()).arg(accessor));
1081 if (it.key() == QLatin1String(
"TRANSLATION"))
1082 node.instanceTranslation = accessor;
1083 else if (it.key() == QLatin1String(
"ROTATION"))
1084 node.instanceRotation = accessor;
1085 else if (it.key() == QLatin1String(
"SCALE"))
1086 node.instanceScale = accessor;
1088 qCWarning(lcQuick3DGltf) <<
"Ignoring unsupported instance attribute" << it.key();
1090 node.hasInstancing = node.instanceTranslation >= 0 || node.instanceRotation >= 0
1091 || node.instanceScale >= 0;
1094 if (node.mesh >= document->meshes.size())
1095 return setError(QStringLiteral(
"Node %1 references invalid mesh %2")
1096 .arg(document->nodes.size()).arg(node.mesh));
1097 if (node.camera >= document->cameras.size())
1098 return setError(QStringLiteral(
"Node %1 references invalid camera %2")
1099 .arg(document->nodes.size()).arg(node.camera));
1100 if (node.light >= document->lights.size())
1101 return setError(QStringLiteral(
"Node %1 references invalid light %2")
1102 .arg(document->nodes.size()).arg(node.light));
1103 document->nodes.append(node);
1111 const int nodeCount = document->nodes.size();
1112 QVarLengthArray<
int, 64> parentCount(nodeCount);
1113 std::fill(parentCount.begin(), parentCount.end(), 0);
1114 for (
int i = 0; i < nodeCount; ++i) {
1115 Node &node = document->nodes[i];
1116 for (qsizetype c = 0; c < node.children.size(); ++c) {
1117 const int child = node.children.at(c);
1118 if (child < 0 || child >= nodeCount)
1119 return setError(QStringLiteral(
"Node %1 references invalid child node %2").arg(i).arg(child));
1120 if (++parentCount[child] > 1) {
1125 qCWarning(lcQuick3DGltf) <<
"Node" << child
1126 <<
"has more than one parent; ignoring the edge from node" << i;
1127 --parentCount[child];
1128 node.children.removeAt(c--);
1136 constexpr int maxNodeDepth = 4096;
1137 QVarLengthArray<
int, 64> parent(nodeCount);
1138 std::fill(parent.begin(), parent.end(), -1);
1139 for (
int i = 0; i < nodeCount; ++i) {
1140 for (
int child : document->nodes.at(i).children)
1143 QVarLengthArray<
int, 64> depth(nodeCount);
1144 std::fill(depth.begin(), depth.end(), -1);
1145 QVarLengthArray<
int, 64> chain;
1146 for (
int i = 0; i < nodeCount; ++i) {
1149 while (n >= 0 && depth[n] < 0 && chain.size() <= nodeCount) {
1153 if (chain.size() > nodeCount)
1154 return setError(QStringLiteral(
"Node hierarchy contains a cycle"));
1155 int d = n >= 0 ? depth[n] : 0;
1156 for (
auto it = chain.rbegin(); it != chain.rend(); ++it)
1158 if (d > maxNodeDepth)
1159 return setError(QStringLiteral(
"Node hierarchy is deeper than %1").arg(maxNodeDepth));
1164 const QJsonArray skins = root.value(QLatin1String(
"skins")).toArray();
1165 for (
const auto &value : skins) {
1166 const QJsonObject object = value.toObject();
1168 skin.name = object.value(QLatin1String(
"name")).toString();
1169 skin.inverseBindMatrices = object.value(QLatin1String(
"inverseBindMatrices")).toInt(-1);
1170 skin.skeleton = object.value(QLatin1String(
"skeleton")).toInt(-1);
1171 const QJsonArray joints = object.value(QLatin1String(
"joints")).toArray();
1172 for (
const auto &joint : joints)
1173 skin.joints.append(joint.toInt(-1));
1175 if (skin.inverseBindMatrices >= document->accessors.size())
1176 return setError(QStringLiteral(
"Skin %1 references invalid accessor %2")
1177 .arg(document->skins.size()).arg(skin.inverseBindMatrices));
1178 for (
int joint : std::as_const(skin.joints)) {
1179 if (joint < 0 || joint >= nodeCount)
1180 return setError(QStringLiteral(
"Skin %1 references invalid joint node %2")
1181 .arg(document->skins.size()).arg(joint));
1183 document->skins.append(skin);
1185 for (
int i = 0; i < nodeCount; ++i) {
1186 if (document->nodes.at(i).skin >= document->skins.size())
1187 return setError(QStringLiteral(
"Node %1 references invalid skin %2")
1188 .arg(i).arg(document->nodes.at(i).skin));
1192 const QJsonArray animations = root.value(QLatin1String(
"animations")).toArray();
1193 for (
const auto &value : animations) {
1194 const QJsonObject object = value.toObject();
1195 Animation animation;
1196 animation.name = object.value(QLatin1String(
"name")).toString();
1198 const QJsonArray animationSamplers = object.value(QLatin1String(
"samplers")).toArray();
1199 for (
const auto &samplerValue : animationSamplers) {
1200 const QJsonObject samplerObject = samplerValue.toObject();
1201 AnimationSampler sampler;
1202 sampler.input = samplerObject.value(QLatin1String(
"input")).toInt(-1);
1203 sampler.output = samplerObject.value(QLatin1String(
"output")).toInt(-1);
1204 const QString interpolation = samplerObject.value(QLatin1String(
"interpolation")).toString();
1205 if (interpolation == QLatin1String(
"STEP"))
1206 sampler.interpolation = AnimationSampler::Interpolation::Step;
1207 else if (interpolation == QLatin1String(
"CUBICSPLINE"))
1208 sampler.interpolation = AnimationSampler::Interpolation::CubicSpline;
1209 if (sampler.input < 0 || sampler.input >= document->accessors.size()
1210 || sampler.output < 0 || sampler.output >= document->accessors.size()) {
1212 QStringLiteral(
"Animation %1 sampler references an invalid accessor")
1213 .arg(document->animations.size()));
1215 animation.samplers.append(sampler);
1218 const QJsonArray channels = object.value(QLatin1String(
"channels")).toArray();
1219 for (
const auto &channelValue : channels) {
1220 const QJsonObject channelObject = channelValue.toObject();
1221 AnimationChannel channel;
1222 channel.sampler = channelObject.value(QLatin1String(
"sampler")).toInt(-1);
1223 const QJsonObject target = channelObject.value(QLatin1String(
"target")).toObject();
1224 channel.targetNode = target.value(QLatin1String(
"node")).toInt(-1);
1225 const QString path = target.value(QLatin1String(
"path")).toString();
1226 if (path == QLatin1String(
"rotation"))
1227 channel.path = AnimationChannel::Path::Rotation;
1228 else if (path == QLatin1String(
"scale"))
1229 channel.path = AnimationChannel::Path::Scale;
1230 else if (path == QLatin1String(
"weights"))
1231 channel.path = AnimationChannel::Path::Weights;
1232 else if (path != QLatin1String(
"translation")) {
1233 qCWarning(lcQuick3DGltf) <<
"Ignoring animation channel with unsupported path" << path;
1236 if (channel.sampler < 0 || channel.sampler >= animation.samplers.size())
1237 return setError(QStringLiteral(
"Animation %1 channel references invalid sampler %2")
1238 .arg(document->animations.size())
1239 .arg(channel.sampler));
1240 if (channel.targetNode >= nodeCount)
1241 return setError(QStringLiteral(
"Animation %1 channel references invalid node %2")
1242 .arg(document->animations.size())
1243 .arg(channel.targetNode));
1244 animation.channels.append(channel);
1247 document->animations.append(animation);
1251 const QJsonArray scenes = root.value(QLatin1String(
"scenes")).toArray();
1252 for (
const auto &value : scenes) {
1253 const QJsonObject object = value.toObject();
1255 scene.name = object.value(QLatin1String(
"name")).toString();
1256 QSet<
int> seenRoots;
1257 const QJsonArray sceneNodes = object.value(QLatin1String(
"nodes")).toArray();
1258 for (
const auto &nodeIndex : sceneNodes) {
1259 const int index = nodeIndex.toInt(-1);
1260 if (index < 0 || index >= nodeCount)
1261 return setError(QStringLiteral(
"Scene %1 references invalid node %2")
1262 .arg(document->scenes.size()).arg(index));
1267 if (parentCount[index] != 0 || seenRoots.contains(index)) {
1268 qCWarning(lcQuick3DGltf) <<
"Scene" << document->scenes.size() <<
"references node" << index
1269 <<
"which is not a unique root node; ignoring it";
1272 seenRoots.insert(index);
1273 scene.nodes.append(index);
1275 document->scenes.append(scene);
1277 document->scene = root.value(QLatin1String(
"scene")).toInt(-1);
1278 if (document->scene >= document->scenes.size())
1279 return setError(QStringLiteral(
"Document references invalid default scene %1").arg(document->scene));