189bool QSSGGltfParser::parse(
const QByteArray &data,
const QString &baseDir, QSSGGltfDocument *document)
192 m_errorMessage.clear();
193 *document = QSSGGltfDocument();
194 document->baseDir = baseDir;
196 QByteArray json = data;
200 if (data.size() >= 12 && qFromLittleEndian<quint32>(data.constData()) == GLB_MAGIC) {
201 const quint32 version = qFromLittleEndian<quint32>(data.constData() + 4);
203 return setError(QStringLiteral(
"Unsupported GLB container version %1").arg(version));
204 const quint32 length = qFromLittleEndian<quint32>(data.constData() + 8);
205 if (qint64(length) > data.size())
206 return setError(QStringLiteral(
"Truncated GLB file: header declares %1 bytes, got %2")
207 .arg(length).arg(data.size()));
211 while (offset + 8 <= qint64(length)) {
212 const quint32 chunkLength = qFromLittleEndian<quint32>(data.constData() + offset);
213 const quint32 chunkType = qFromLittleEndian<quint32>(data.constData() + offset + 4);
215 if (offset + qint64(chunkLength) > qint64(length))
216 return setError(QStringLiteral(
"Truncated GLB chunk at offset %1").arg(offset - 8));
218 if (chunkType == GLB_CHUNK_JSON && json.isEmpty())
219 json = data.mid(offset, chunkLength);
220 else if (chunkType == GLB_CHUNK_BIN && binChunk.isEmpty())
221 binChunk = data.mid(offset, chunkLength);
223 qCWarning(lcQuick3DGltf) <<
"Skipping unknown GLB chunk type" << Qt::hex << chunkType;
226 offset += chunkLength;
228 offset += 4 - (offset % 4);
231 return setError(QStringLiteral(
"GLB container has no JSON chunk"));
234 QJsonParseError jsonError;
235 const QJsonDocument jsonDocument = QJsonDocument::fromJson(json, &jsonError);
236 if (jsonDocument.isNull())
237 return setError(QStringLiteral(
"Invalid glTF JSON: %1").arg(jsonError.errorString()));
238 if (!jsonDocument.isObject())
239 return setError(QStringLiteral(
"Invalid glTF JSON: root is not an object"));
241 const QJsonObject root = jsonDocument.object();
245 const QJsonValue assetValue = root.value(QLatin1String(
"asset"));
246 if (!assetValue.isObject())
247 return setError(QStringLiteral(
"Not a glTF document: no asset object"));
248 const QJsonObject asset = assetValue.toObject();
249 document->asset.version = asset.value(QLatin1String(
"version")).toString();
250 document->asset.minVersion = asset.value(QLatin1String(
"minVersion")).toString();
251 document->asset.generator = asset.value(QLatin1String(
"generator")).toString();
252 document->asset.copyright = asset.value(QLatin1String(
"copyright")).toString();
254 const int major = document->asset.version.section(QLatin1Char(
'.'), 0, 0).toInt();
256 return setError(QStringLiteral(
"Unsupported glTF version '%1'").arg(document->asset.version));
261 if (!document->asset.minVersion.isEmpty()) {
262 const int minMajor = document->asset.minVersion.section(QLatin1Char(
'.'), 0, 0).toInt();
263 const int minMinor = document->asset.minVersion.section(QLatin1Char(
'.'), 1, 1).toInt();
264 if (minMajor != 2 || minMinor > 0)
265 return setError(QStringLiteral(
"Asset requires glTF version '%1' or higher")
266 .arg(document->asset.minVersion));
271 for (
const auto &value : root.value(QLatin1String(
"extensionsUsed")).toArray())
272 document->extensionsUsed.append(value.toString());
273 for (
const auto &value : root.value(QLatin1String(
"extensionsRequired")).toArray())
274 document->extensionsRequired.append(value.toString());
275 document->rootExtensions = root.value(QLatin1String(
"extensions")).toObject();
277 const QStringList supported = supportedExtensions();
278 for (
const QString &required : std::as_const(document->extensionsRequired)) {
279 if (!supported.contains(required))
280 return setError(QStringLiteral(
"Asset requires unsupported glTF extension '%1'").arg(required));
282 for (
const QString &used : std::as_const(document->extensionsUsed)) {
283 if (!supported.contains(used))
284 qCWarning(lcQuick3DGltf) <<
"Ignoring unsupported glTF extension" << used;
288 for (
const auto &value : root.value(QLatin1String(
"buffers")).toArray()) {
289 const QJsonObject object = value.toObject();
291 buffer.uri = object.value(QLatin1String(
"uri")).toString();
292 buffer.byteLength = qint64(object.value(QLatin1String(
"byteLength")).toDouble());
293 if (buffer.byteLength < 0)
294 return setError(QStringLiteral(
"Buffer %1 has negative byte length").arg(document->buffers.size()));
295 if (buffer.uri.isEmpty()) {
297 if (document->buffers.isEmpty() && !binChunk.isEmpty())
298 buffer.data = binChunk;
299 else if (binChunk.isEmpty())
301 QStringLiteral(
"Buffer %1 has no URI and there is no GLB BIN chunk")
302 .arg(document->buffers.size()));
304 return setError(QStringLiteral(
"Only the first buffer may refer to the GLB BIN chunk"));
306 QString resolveError;
307 buffer.data = QSSGGltfResourceResolver::loadUri(buffer.uri, baseDir, &resolveError);
311 if (!resolveError.isEmpty())
312 return setError(QStringLiteral(
"Failed to load buffer %1: %2")
313 .arg(document->buffers.size()).arg(resolveError));
315 if (buffer.data.size() < buffer.byteLength)
316 return setError(QStringLiteral(
"Buffer %1 is %2 bytes, expected at least %3")
317 .arg(document->buffers.size()).arg(buffer.data.size()).arg(buffer.byteLength));
318 document->buffers.append(buffer);
322 for (
const auto &value : root.value(QLatin1String(
"bufferViews")).toArray()) {
323 const QJsonObject object = value.toObject();
325 view.buffer = object.value(QLatin1String(
"buffer")).toInt(-1);
326 view.byteOffset = qint64(object.value(QLatin1String(
"byteOffset")).toDouble(0));
327 view.byteLength = qint64(object.value(QLatin1String(
"byteLength")).toDouble());
328 view.byteStride = object.value(QLatin1String(
"byteStride")).toInt(0);
329 view.target = object.value(QLatin1String(
"target")).toInt(0);
330 view.name = object.value(QLatin1String(
"name")).toString();
332 if (view.buffer < 0 || view.buffer >= document->buffers.size())
334 QStringLiteral(
"Buffer view %1 references invalid buffer %2")
335 .arg(document->bufferViews.size()).arg(view.buffer));
339 if (view.byteOffset < 0 || view.byteLength < 0 || view.byteStride < 0 || view.byteStride > 252)
340 return setError(QStringLiteral(
"Buffer view %1 has an invalid byte offset, length, or stride")
341 .arg(document->bufferViews.size()));
345 const qint64 bufferLength = document->buffers.at(view.buffer).byteLength;
346 if (view.byteLength > bufferLength || view.byteOffset > bufferLength - view.byteLength)
348 QStringLiteral(
"Buffer view %1 extends past the end of buffer %2")
349 .arg(document->bufferViews.size()).arg(view.buffer));
350 document->bufferViews.append(view);
354 for (
const auto &value : root.value(QLatin1String(
"accessors")).toArray()) {
355 const QJsonObject object = value.toObject();
357 accessor.bufferView = object.value(QLatin1String(
"bufferView")).toInt(-1);
358 accessor.byteOffset = qint64(object.value(QLatin1String(
"byteOffset")).toDouble(0));
359 const int componentType = object.value(QLatin1String(
"componentType")).toInt();
360 if (!isValidComponentType(componentType))
361 return setError(QStringLiteral(
"Accessor %1 has invalid component type %2")
362 .arg(document->accessors.size()).arg(componentType));
363 accessor.componentType = Accessor::ComponentType(componentType);
365 accessor.type = accessorTypeFromString(object.value(QLatin1String(
"type")).toString(), &typeOk);
367 return setError(QStringLiteral(
"Accessor %1 has invalid type '%2'")
368 .arg(document->accessors.size())
369 .arg(object.value(QLatin1String(
"type")).toString()));
378 constexpr qint64 maxAccessorElements = qint64(256) * 1024 * 1024;
379 const double rawCount = object.value(QLatin1String(
"count")).toDouble();
380 if (!(rawCount >= 0.0 && rawCount <=
double(maxAccessorElements)))
381 return setError(QStringLiteral(
"Accessor %1 has a negative or unreasonably large count")
382 .arg(document->accessors.size()));
383 accessor.count = qint64(rawCount);
384 if (accessor.byteOffset < 0)
385 return setError(QStringLiteral(
"Accessor %1 has a negative byte offset")
386 .arg(document->accessors.size()));
387 accessor.normalized = object.value(QLatin1String(
"normalized")).toBool(
false);
388 accessor.name = object.value(QLatin1String(
"name")).toString();
389 for (
const auto &m : object.value(QLatin1String(
"min")).toArray())
390 accessor.min.append(m.toDouble());
391 for (
const auto &m : object.value(QLatin1String(
"max")).toArray())
392 accessor.max.append(m.toDouble());
394 const QJsonValue sparseValue = object.value(QLatin1String(
"sparse"));
395 if (sparseValue.isObject()) {
396 const QJsonObject sparseObject = sparseValue.toObject();
397 Accessor::Sparse sparse;
400 const double rawSparseCount = sparseObject.value(QLatin1String(
"count")).toDouble();
401 if (!(rawSparseCount >= 0.0 && rawSparseCount <=
double(maxAccessorElements)))
402 return setError(QStringLiteral(
"Accessor %1 has a negative or unreasonably large sparse count")
403 .arg(document->accessors.size()));
404 sparse.count = qint64(rawSparseCount);
405 const QJsonObject indices = sparseObject.value(QLatin1String(
"indices")).toObject();
406 sparse.indicesBufferView = indices.value(QLatin1String(
"bufferView")).toInt(-1);
407 sparse.indicesByteOffset = qint64(indices.value(QLatin1String(
"byteOffset")).toDouble(0));
408 const int indicesComponentType = indices.value(QLatin1String(
"componentType")).toInt();
409 if (!isValidComponentType(indicesComponentType))
411 QStringLiteral(
"Accessor %1 sparse indices have invalid component type")
412 .arg(document->accessors.size()));
413 sparse.indicesComponentType = Accessor::ComponentType(indicesComponentType);
414 const QJsonObject values = sparseObject.value(QLatin1String(
"values")).toObject();
415 sparse.valuesBufferView = values.value(QLatin1String(
"bufferView")).toInt(-1);
416 sparse.valuesByteOffset = qint64(values.value(QLatin1String(
"byteOffset")).toDouble(0));
418 if (sparse.indicesBufferView < 0 || sparse.indicesBufferView >= document->bufferViews.size()
419 || sparse.valuesBufferView < 0 || sparse.valuesBufferView >= document->bufferViews.size()) {
420 return setError(QStringLiteral(
"Accessor %1 sparse data references an invalid buffer view")
421 .arg(document->accessors.size()));
423 if (sparse.count > accessor.count
424 || sparse.indicesByteOffset < 0 || sparse.valuesByteOffset < 0) {
425 return setError(QStringLiteral(
"Accessor %1 has an invalid sparse count or byte offset")
426 .arg(document->accessors.size()));
429 const BufferView &indicesView = document->bufferViews.at(sparse.indicesBufferView);
430 const BufferView &valuesView = document->bufferViews.at(sparse.valuesBufferView);
431 const qint64 indicesSpan =
432 sparse.count * Accessor::componentByteSize(sparse.indicesComponentType);
433 const qint64 valuesSpan = sparse.count * accessor.elementByteSize();
434 if (indicesSpan > indicesView.byteLength
435 || sparse.indicesByteOffset > indicesView.byteLength - indicesSpan
436 || valuesSpan > valuesView.byteLength
437 || sparse.valuesByteOffset > valuesView.byteLength - valuesSpan) {
439 QStringLiteral(
"Accessor %1 sparse data extends past the end of its buffer view")
440 .arg(document->accessors.size()));
442 accessor.sparse = sparse;
445 if (accessor.bufferView >= document->bufferViews.size())
447 QStringLiteral(
"Accessor %1 references invalid buffer view %2")
448 .arg(document->accessors.size()).arg(accessor.bufferView));
449 if (accessor.bufferView >= 0) {
450 const BufferView &view = document->bufferViews.at(accessor.bufferView);
451 const qint64 elementSize = accessor.elementByteSize();
452 const qint64 stride = view.byteStride > 0 ? view.byteStride : elementSize;
454 const qint64 span = (accessor.count - 1) * stride + elementSize;
455 if (accessor.count > 0
456 && (span > view.byteLength || accessor.byteOffset > view.byteLength - span))
457 return setError(QStringLiteral(
"Accessor %1 extends past the end of buffer view %2")
458 .arg(document->accessors.size())
459 .arg(accessor.bufferView));
465 constexpr qint64 maxSyntheticAccessorBytes = qint64(256) * 1024 * 1024;
466 if (accessor.count * accessor.elementByteSize() > maxSyntheticAccessorBytes)
467 return setError(QStringLiteral(
"Accessor %1 has no buffer view and an unreasonably large count")
468 .arg(document->accessors.size()));
470 document->accessors.append(accessor);
474 for (
const auto &value : root.value(QLatin1String(
"images")).toArray()) {
475 const QJsonObject object = value.toObject();
477 image.uri = object.value(QLatin1String(
"uri")).toString();
478 image.bufferView = object.value(QLatin1String(
"bufferView")).toInt(-1);
479 image.mimeType = object.value(QLatin1String(
"mimeType")).toString();
480 image.name = object.value(QLatin1String(
"name")).toString();
481 if (image.bufferView >= document->bufferViews.size())
482 return setError(QStringLiteral(
"Image %1 references invalid buffer view %2")
483 .arg(document->images.size()).arg(image.bufferView));
484 document->images.append(image);
488 for (
const auto &value : root.value(QLatin1String(
"samplers")).toArray()) {
489 const QJsonObject object = value.toObject();
491 sampler.magFilter = object.value(QLatin1String(
"magFilter")).toInt(0);
492 sampler.minFilter = object.value(QLatin1String(
"minFilter")).toInt(0);
493 sampler.wrapS = object.value(QLatin1String(
"wrapS")).toInt(Sampler::Repeat);
494 sampler.wrapT = object.value(QLatin1String(
"wrapT")).toInt(Sampler::Repeat);
495 sampler.name = object.value(QLatin1String(
"name")).toString();
496 document->samplers.append(sampler);
500 for (
const auto &value : root.value(QLatin1String(
"textures")).toArray()) {
501 const QJsonObject object = value.toObject();
503 texture.sampler = object.value(QLatin1String(
"sampler")).toInt(-1);
504 texture.source = object.value(QLatin1String(
"source")).toInt(-1);
505 texture.name = object.value(QLatin1String(
"name")).toString();
506 texture.extensions = object.value(QLatin1String(
"extensions")).toObject();
507 if (texture.sampler >= document->samplers.size())
508 return setError(QStringLiteral(
"Texture %1 references invalid sampler %2")
509 .arg(document->textures.size()).arg(texture.sampler));
510 if (texture.source >= document->images.size())
511 return setError(QStringLiteral(
"Texture %1 references invalid image %2")
512 .arg(document->textures.size()).arg(texture.source));
513 document->textures.append(texture);
522 int badTextureIndex = -1;
523 const auto parseTexture = [&](
const QJsonObject &object,
524 const char *scaleOrStrengthKey =
nullptr) {
525 const TextureInfo info = parseTextureInfo(object, scaleOrStrengthKey);
526 if (info.index >= document->textures.size())
527 badTextureIndex = info.index;
531 for (
const auto &value : root.value(QLatin1String(
"materials")).toArray()) {
532 const QJsonObject object = value.toObject();
534 material.name = object.value(QLatin1String(
"name")).toString();
536 const QJsonValue pbrValue = object.value(QLatin1String(
"pbrMetallicRoughness"));
537 if (pbrValue.isObject()) {
538 const QJsonObject pbr = pbrValue.toObject();
539 material.baseColorFactor =
540 toVector4D(pbr.value(QLatin1String(
"baseColorFactor")).toArray(), material.baseColorFactor);
541 material.baseColorTexture = parseTexture(pbr.value(QLatin1String(
"baseColorTexture")).toObject());
542 material.metallicFactor =
float(pbr.value(QLatin1String(
"metallicFactor")).toDouble(1.0));
543 material.roughnessFactor =
float(pbr.value(QLatin1String(
"roughnessFactor")).toDouble(1.0));
544 material.metallicRoughnessTexture =
545 parseTexture(pbr.value(QLatin1String(
"metallicRoughnessTexture")).toObject());
548 material.normalTexture = parseTexture(object.value(QLatin1String(
"normalTexture")).toObject(),
"scale");
549 material.occlusionTexture =
550 parseTexture(object.value(QLatin1String(
"occlusionTexture")).toObject(),
"strength");
551 material.emissiveTexture = parseTexture(object.value(QLatin1String(
"emissiveTexture")).toObject());
552 material.emissiveFactor =
553 toVector3D(object.value(QLatin1String(
"emissiveFactor")).toArray(), material.emissiveFactor);
555 const QString alphaMode = object.value(QLatin1String(
"alphaMode")).toString();
556 if (alphaMode == QLatin1String(
"MASK"))
557 material.alphaMode = Material::AlphaMode::Mask;
558 else if (alphaMode == QLatin1String(
"BLEND"))
559 material.alphaMode = Material::AlphaMode::Blend;
560 material.alphaCutoff =
float(object.value(QLatin1String(
"alphaCutoff")).toDouble(0.5));
561 material.doubleSided = object.value(QLatin1String(
"doubleSided")).toBool(
false);
563 material.extensions = object.value(QLatin1String(
"extensions")).toObject();
564 const QJsonObject &ext = material.extensions;
566 material.unlit = ext.contains(QLatin1String(
"KHR_materials_unlit"));
568 const QJsonValue sgValue = ext.value(QLatin1String(
"KHR_materials_pbrSpecularGlossiness"));
569 if (sgValue.isObject()) {
570 const QJsonObject sg = sgValue.toObject();
571 Material::SpecularGlossiness specularGlossiness;
572 specularGlossiness.diffuseFactor = toVector4D(sg.value(QLatin1String(
"diffuseFactor")).toArray(),
573 specularGlossiness.diffuseFactor);
574 specularGlossiness.diffuseTexture = parseTexture(sg.value(QLatin1String(
"diffuseTexture")).toObject());
575 specularGlossiness.specularFactor = toVector3D(sg.value(QLatin1String(
"specularFactor")).toArray(),
576 specularGlossiness.specularFactor);
577 specularGlossiness.glossinessFactor =
float(sg.value(QLatin1String(
"glossinessFactor")).toDouble(1.0));
578 specularGlossiness.specularGlossinessTexture = parseTexture(
579 sg.value(QLatin1String(
"specularGlossinessTexture")).toObject());
580 material.specularGlossiness = specularGlossiness;
583 const QJsonValue ccValue = ext.value(QLatin1String(
"KHR_materials_clearcoat"));
584 if (ccValue.isObject()) {
585 const QJsonObject cc = ccValue.toObject();
586 Material::Clearcoat clearcoat;
587 clearcoat.clearcoatFactor =
float(cc.value(QLatin1String(
"clearcoatFactor")).toDouble(0.0));
588 clearcoat.clearcoatTexture = parseTexture(cc.value(QLatin1String(
"clearcoatTexture")).toObject());
589 clearcoat.clearcoatRoughnessFactor =
590 float(cc.value(QLatin1String(
"clearcoatRoughnessFactor")).toDouble(0.0));
591 clearcoat.clearcoatRoughnessTexture =
592 parseTexture(cc.value(QLatin1String(
"clearcoatRoughnessTexture")).toObject());
593 clearcoat.clearcoatNormalTexture =
594 parseTexture(cc.value(QLatin1String(
"clearcoatNormalTexture")).toObject(),
"scale");
595 material.clearcoat = clearcoat;
598 const QJsonValue trValue = ext.value(QLatin1String(
"KHR_materials_transmission"));
599 if (trValue.isObject()) {
600 const QJsonObject tr = trValue.toObject();
601 Material::Transmission transmission;
602 transmission.transmissionFactor =
float(tr.value(QLatin1String(
"transmissionFactor")).toDouble(0.0));
603 transmission.transmissionTexture =
604 parseTexture(tr.value(QLatin1String(
"transmissionTexture")).toObject());
605 material.transmission = transmission;
608 const QJsonValue volValue = ext.value(QLatin1String(
"KHR_materials_volume"));
609 if (volValue.isObject()) {
610 const QJsonObject vol = volValue.toObject();
611 Material::Volume volume;
612 volume.thicknessFactor =
float(vol.value(QLatin1String(
"thicknessFactor")).toDouble(0.0));
613 volume.thicknessTexture = parseTexture(vol.value(QLatin1String(
"thicknessTexture")).toObject());
614 volume.attenuationDistance =
float(vol.value(QLatin1String(
"attenuationDistance")).toDouble(0.0));
615 volume.attenuationColor =
616 toVector3D(vol.value(QLatin1String(
"attenuationColor")).toArray(), volume.attenuationColor);
617 material.volume = volume;
620 const QJsonValue iorValue = ext.value(QLatin1String(
"KHR_materials_ior"));
621 if (iorValue.isObject())
622 material.ior =
float(iorValue.toObject().value(QLatin1String(
"ior")).toDouble(1.5));
624 const QJsonValue esValue = ext.value(QLatin1String(
"KHR_materials_emissive_strength"));
625 if (esValue.isObject())
626 material.emissiveStrength =
627 float(esValue.toObject().value(QLatin1String(
"emissiveStrength")).toDouble(1.0));
629 const QJsonValue spValue = ext.value(QLatin1String(
"KHR_materials_specular"));
630 if (spValue.isObject()) {
631 const QJsonObject sp = spValue.toObject();
632 Material::Specular specular;
633 specular.specularFactor =
float(sp.value(QLatin1String(
"specularFactor")).toDouble(1.0));
634 specular.specularTexture = parseTexture(sp.value(QLatin1String(
"specularTexture")).toObject());
635 specular.specularColorFactor = toVector3D(sp.value(QLatin1String(
"specularColorFactor")).toArray(),
636 specular.specularColorFactor);
637 specular.specularColorTexture =
638 parseTexture(sp.value(QLatin1String(
"specularColorTexture")).toObject());
639 material.specular = specular;
642 document->materials.append(material);
645 if (badTextureIndex >= 0)
646 return setError(QStringLiteral(
"Material references invalid texture %1").arg(badTextureIndex));
649 for (
const auto &value : root.value(QLatin1String(
"meshes")).toArray()) {
650 const QJsonObject object = value.toObject();
652 mesh.name = object.value(QLatin1String(
"name")).toString();
653 for (
const auto &w : object.value(QLatin1String(
"weights")).toArray())
654 mesh.weights.append(
float(w.toDouble()));
656 for (
const auto &primitiveValue : object.value(QLatin1String(
"primitives")).toArray()) {
657 const QJsonObject primitiveObject = primitiveValue.toObject();
658 MeshPrimitive primitive;
659 const QJsonObject attributes = primitiveObject.value(QLatin1String(
"attributes")).toObject();
660 for (
auto it = attributes.constBegin(); it != attributes.constEnd(); ++it)
661 primitive.attributes.insert(it.key().toUtf8(), it.value().toInt(-1));
662 primitive.indices = primitiveObject.value(QLatin1String(
"indices")).toInt(-1);
663 primitive.material = primitiveObject.value(QLatin1String(
"material")).toInt(-1);
664 primitive.mode = primitiveObject.value(QLatin1String(
"mode")).toInt(MeshPrimitive::Triangles);
665 for (
const auto &targetValue : primitiveObject.value(QLatin1String(
"targets")).toArray()) {
666 const QJsonObject targetObject = targetValue.toObject();
667 QHash<QByteArray,
int> target;
668 for (
auto it = targetObject.constBegin(); it != targetObject.constEnd(); ++it)
669 target.insert(it.key().toUtf8(), it.value().toInt(-1));
670 primitive.targets.append(target);
672 primitive.extensions = primitiveObject.value(QLatin1String(
"extensions")).toObject();
678 const auto checkAccessor = [&](
int accessor,
const char *what) {
679 if (accessor >= document->accessors.size())
680 return setError(QStringLiteral(
"Mesh %1 primitive references invalid %2 accessor %3")
681 .arg(document->meshes.size()).arg(QLatin1String(what)).arg(accessor));
684 for (
auto it = primitive.attributes.constBegin(); it != primitive.attributes.constEnd(); ++it) {
685 if (!checkAccessor(it.value(), it.key().constData()))
688 if (primitive.indices >= 0 && !checkAccessor(primitive.indices,
"index"))
690 if (primitive.material >= document->materials.size())
691 return setError(QStringLiteral(
"Mesh %1 primitive references invalid material %2")
692 .arg(document->meshes.size())
693 .arg(primitive.material));
695 mesh.primitives.append(primitive);
697 if (mesh.primitives.isEmpty())
698 qCWarning(lcQuick3DGltf) <<
"Mesh" << document->meshes.size() <<
"has no primitives";
699 document->meshes.append(mesh);
703 for (
const auto &value : root.value(QLatin1String(
"cameras")).toArray()) {
704 const QJsonObject object = value.toObject();
706 camera.name = object.value(QLatin1String(
"name")).toString();
707 const QString type = object.value(QLatin1String(
"type")).toString();
708 if (type == QLatin1String(
"orthographic")) {
709 camera.type = Camera::Type::Orthographic;
710 const QJsonObject ortho = object.value(QLatin1String(
"orthographic")).toObject();
711 camera.xmag =
float(ortho.value(QLatin1String(
"xmag")).toDouble());
712 camera.ymag =
float(ortho.value(QLatin1String(
"ymag")).toDouble());
713 camera.znear =
float(ortho.value(QLatin1String(
"znear")).toDouble());
714 camera.zfar =
float(ortho.value(QLatin1String(
"zfar")).toDouble());
716 camera.type = Camera::Type::Perspective;
717 const QJsonObject perspective = object.value(QLatin1String(
"perspective")).toObject();
718 camera.aspectRatio =
float(perspective.value(QLatin1String(
"aspectRatio")).toDouble(0.0));
719 camera.yfov =
float(perspective.value(QLatin1String(
"yfov")).toDouble());
720 camera.znear =
float(perspective.value(QLatin1String(
"znear")).toDouble());
721 camera.zfar =
float(perspective.value(QLatin1String(
"zfar")).toDouble(0.0));
723 document->cameras.append(camera);
728 const QJsonValue lightsExt = document->rootExtensions.value(QLatin1String(
"KHR_lights_punctual"));
729 if (lightsExt.isObject()) {
730 for (
const auto &value : lightsExt.toObject().value(QLatin1String(
"lights")).toArray()) {
731 const QJsonObject object = value.toObject();
733 light.name = object.value(QLatin1String(
"name")).toString();
734 const QString type = object.value(QLatin1String(
"type")).toString();
735 if (type == QLatin1String(
"point"))
736 light.type = Light::Type::Point;
737 else if (type == QLatin1String(
"spot"))
738 light.type = Light::Type::Spot;
740 light.type = Light::Type::Directional;
741 light.color = toVector3D(object.value(QLatin1String(
"color")).toArray(), light.color);
742 light.intensity =
float(object.value(QLatin1String(
"intensity")).toDouble(1.0));
743 light.range =
float(object.value(QLatin1String(
"range")).toDouble(0.0));
744 if (light.type == Light::Type::Spot) {
745 const QJsonObject spot = object.value(QLatin1String(
"spot")).toObject();
746 light.innerConeAngle =
float(spot.value(QLatin1String(
"innerConeAngle")).toDouble(0.0));
747 light.outerConeAngle =
float(spot.value(QLatin1String(
"outerConeAngle")).toDouble(M_PI_4));
749 document->lights.append(light);
755 for (
const auto &value : root.value(QLatin1String(
"nodes")).toArray()) {
756 const QJsonObject object = value.toObject();
758 node.name = object.value(QLatin1String(
"name")).toString();
759 for (
const auto &child : object.value(QLatin1String(
"children")).toArray())
760 node.children.append(child.toInt(-1));
761 node.mesh = object.value(QLatin1String(
"mesh")).toInt(-1);
762 node.skin = object.value(QLatin1String(
"skin")).toInt(-1);
763 node.camera = object.value(QLatin1String(
"camera")).toInt(-1);
765 const QJsonValue matrixValue = object.value(QLatin1String(
"matrix"));
766 if (matrixValue.isArray()) {
767 const QJsonArray m = matrixValue.toArray();
768 if (m.size() == 16) {
770 for (
int i = 0; i < 16; ++i)
771 values[i] =
float(m.at(i).toDouble());
773 node.matrix = QMatrix4x4(values).transposed();
774 node.hasMatrix =
true;
777 node.translation = toVector3D(object.value(QLatin1String(
"translation")).toArray(), node.translation);
778 const QJsonArray rotation = object.value(QLatin1String(
"rotation")).toArray();
779 if (rotation.size() == 4) {
781 node.rotation = QQuaternion(
float(rotation.at(3).toDouble()),
float(rotation.at(0).toDouble()),
782 float(rotation.at(1).toDouble()),
float(rotation.at(2).toDouble()));
784 node.scale = toVector3D(object.value(QLatin1String(
"scale")).toArray(), node.scale);
785 for (
const auto &w : object.value(QLatin1String(
"weights")).toArray())
786 node.weights.append(
float(w.toDouble()));
787 node.extensions = object.value(QLatin1String(
"extensions")).toObject();
789 const QJsonValue lightExt = node.extensions.value(QLatin1String(
"KHR_lights_punctual"));
790 if (lightExt.isObject())
791 node.light = lightExt.toObject().value(QLatin1String(
"light")).toInt(-1);
793 if (node.mesh >= document->meshes.size())
794 return setError(QStringLiteral(
"Node %1 references invalid mesh %2")
795 .arg(document->nodes.size()).arg(node.mesh));
796 if (node.camera >= document->cameras.size())
797 return setError(QStringLiteral(
"Node %1 references invalid camera %2")
798 .arg(document->nodes.size()).arg(node.camera));
799 if (node.light >= document->lights.size())
800 return setError(QStringLiteral(
"Node %1 references invalid light %2")
801 .arg(document->nodes.size()).arg(node.light));
802 document->nodes.append(node);
810 const int nodeCount = document->nodes.size();
811 QVarLengthArray<
int, 64> parentCount(nodeCount);
812 std::fill(parentCount.begin(), parentCount.end(), 0);
813 for (
int i = 0; i < nodeCount; ++i) {
814 Node &node = document->nodes[i];
815 for (qsizetype c = 0; c < node.children.size(); ++c) {
816 const int child = node.children.at(c);
817 if (child < 0 || child >= nodeCount)
818 return setError(QStringLiteral(
"Node %1 references invalid child node %2").arg(i).arg(child));
819 if (++parentCount[child] > 1) {
824 qCWarning(lcQuick3DGltf) <<
"Node" << child
825 <<
"has more than one parent; ignoring the edge from node" << i;
826 --parentCount[child];
827 node.children.removeAt(c--);
835 constexpr int maxNodeDepth = 4096;
836 QVarLengthArray<
int, 64> parent(nodeCount);
837 std::fill(parent.begin(), parent.end(), -1);
838 for (
int i = 0; i < nodeCount; ++i) {
839 for (
int child : document->nodes.at(i).children)
842 QVarLengthArray<
int, 64> depth(nodeCount);
843 std::fill(depth.begin(), depth.end(), -1);
844 QVarLengthArray<
int, 64> chain;
845 for (
int i = 0; i < nodeCount; ++i) {
848 while (n >= 0 && depth[n] < 0 && chain.size() <= nodeCount) {
852 if (chain.size() > nodeCount)
853 return setError(QStringLiteral(
"Node hierarchy contains a cycle"));
854 int d = n >= 0 ? depth[n] : 0;
855 for (
auto it = chain.rbegin(); it != chain.rend(); ++it)
857 if (d > maxNodeDepth)
858 return setError(QStringLiteral(
"Node hierarchy is deeper than %1").arg(maxNodeDepth));
863 for (
const auto &value : root.value(QLatin1String(
"skins")).toArray()) {
864 const QJsonObject object = value.toObject();
866 skin.name = object.value(QLatin1String(
"name")).toString();
867 skin.inverseBindMatrices = object.value(QLatin1String(
"inverseBindMatrices")).toInt(-1);
868 skin.skeleton = object.value(QLatin1String(
"skeleton")).toInt(-1);
869 for (
const auto &joint : object.value(QLatin1String(
"joints")).toArray())
870 skin.joints.append(joint.toInt(-1));
872 if (skin.inverseBindMatrices >= document->accessors.size())
873 return setError(QStringLiteral(
"Skin %1 references invalid accessor %2")
874 .arg(document->skins.size()).arg(skin.inverseBindMatrices));
875 for (
int joint : std::as_const(skin.joints)) {
876 if (joint < 0 || joint >= nodeCount)
877 return setError(QStringLiteral(
"Skin %1 references invalid joint node %2")
878 .arg(document->skins.size()).arg(joint));
880 document->skins.append(skin);
882 for (
int i = 0; i < nodeCount; ++i) {
883 if (document->nodes.at(i).skin >= document->skins.size())
884 return setError(QStringLiteral(
"Node %1 references invalid skin %2")
885 .arg(i).arg(document->nodes.at(i).skin));
889 for (
const auto &value : root.value(QLatin1String(
"animations")).toArray()) {
890 const QJsonObject object = value.toObject();
892 animation.name = object.value(QLatin1String(
"name")).toString();
894 for (
const auto &samplerValue : object.value(QLatin1String(
"samplers")).toArray()) {
895 const QJsonObject samplerObject = samplerValue.toObject();
896 AnimationSampler sampler;
897 sampler.input = samplerObject.value(QLatin1String(
"input")).toInt(-1);
898 sampler.output = samplerObject.value(QLatin1String(
"output")).toInt(-1);
899 const QString interpolation = samplerObject.value(QLatin1String(
"interpolation")).toString();
900 if (interpolation == QLatin1String(
"STEP"))
901 sampler.interpolation = AnimationSampler::Interpolation::Step;
902 else if (interpolation == QLatin1String(
"CUBICSPLINE"))
903 sampler.interpolation = AnimationSampler::Interpolation::CubicSpline;
904 if (sampler.input < 0 || sampler.input >= document->accessors.size()
905 || sampler.output < 0 || sampler.output >= document->accessors.size()) {
907 QStringLiteral(
"Animation %1 sampler references an invalid accessor")
908 .arg(document->animations.size()));
910 animation.samplers.append(sampler);
913 for (
const auto &channelValue : object.value(QLatin1String(
"channels")).toArray()) {
914 const QJsonObject channelObject = channelValue.toObject();
915 AnimationChannel channel;
916 channel.sampler = channelObject.value(QLatin1String(
"sampler")).toInt(-1);
917 const QJsonObject target = channelObject.value(QLatin1String(
"target")).toObject();
918 channel.targetNode = target.value(QLatin1String(
"node")).toInt(-1);
919 const QString path = target.value(QLatin1String(
"path")).toString();
920 if (path == QLatin1String(
"rotation"))
921 channel.path = AnimationChannel::Path::Rotation;
922 else if (path == QLatin1String(
"scale"))
923 channel.path = AnimationChannel::Path::Scale;
924 else if (path == QLatin1String(
"weights"))
925 channel.path = AnimationChannel::Path::Weights;
926 else if (path != QLatin1String(
"translation")) {
927 qCWarning(lcQuick3DGltf) <<
"Ignoring animation channel with unsupported path" << path;
930 if (channel.sampler < 0 || channel.sampler >= animation.samplers.size())
931 return setError(QStringLiteral(
"Animation %1 channel references invalid sampler %2")
932 .arg(document->animations.size())
933 .arg(channel.sampler));
934 if (channel.targetNode >= nodeCount)
935 return setError(QStringLiteral(
"Animation %1 channel references invalid node %2")
936 .arg(document->animations.size())
937 .arg(channel.targetNode));
938 animation.channels.append(channel);
941 document->animations.append(animation);
945 for (
const auto &value : root.value(QLatin1String(
"scenes")).toArray()) {
946 const QJsonObject object = value.toObject();
948 scene.name = object.value(QLatin1String(
"name")).toString();
950 for (
const auto &nodeIndex : object.value(QLatin1String(
"nodes")).toArray()) {
951 const int index = nodeIndex.toInt(-1);
952 if (index < 0 || index >= nodeCount)
953 return setError(QStringLiteral(
"Scene %1 references invalid node %2")
954 .arg(document->scenes.size()).arg(index));
959 if (parentCount[index] != 0 || seenRoots.contains(index)) {
960 qCWarning(lcQuick3DGltf) <<
"Scene" << document->scenes.size() <<
"references node" << index
961 <<
"which is not a unique root node; ignoring it";
964 seenRoots.insert(index);
965 scene.nodes.append(index);
967 document->scenes.append(scene);
969 document->scene = root.value(QLatin1String(
"scene")).toInt(-1);
970 if (document->scene >= document->scenes.size())
971 return setError(QStringLiteral(
"Document references invalid default scene %1").arg(document->scene));