Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qssggltfparser.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
7
8#include <QtCore/qendian.h>
9#include <QtCore/qfile.h>
10#include <QtCore/qfileinfo.h>
11#include <QtCore/qjsonarray.h>
12#include <QtCore/qjsondocument.h>
13#include <QtCore/qset.h>
14#include <QtCore/qvarlengtharray.h>
15
17
18Q_LOGGING_CATEGORY(lcQuick3DGltf, "qt.quick3d.gltf")
19
20using namespace QSSGGltf;
21
22namespace {
23
24// GLB container constants
25constexpr quint32 GLB_MAGIC = 0x46546C67; // 'glTF'
26constexpr quint32 GLB_CHUNK_JSON = 0x4E4F534A; // 'JSON'
27constexpr quint32 GLB_CHUNK_BIN = 0x004E4942; // 'BIN\0'
28
29QVector3D toVector3D(const QJsonArray &array, const QVector3D &defaultValue = {})
30{
31 if (array.size() < 3)
32 return defaultValue;
33 return QVector3D(float(array.at(0).toDouble()), float(array.at(1).toDouble()), float(array.at(2).toDouble()));
34}
35
36QVector4D toVector4D(const QJsonArray &array, const QVector4D &defaultValue = {})
37{
38 if (array.size() < 4)
39 return defaultValue;
40 return QVector4D(float(array.at(0).toDouble()), float(array.at(1).toDouble()),
41 float(array.at(2).toDouble()), float(array.at(3).toDouble()));
42}
43
44TextureInfo parseTextureInfo(const QJsonObject &object, const char *scaleOrStrengthKey = nullptr)
45{
46 TextureInfo info;
47 if (object.isEmpty())
48 return info;
49 info.index = object.value(QLatin1String("index")).toInt(-1);
50 info.texCoord = object.value(QLatin1String("texCoord")).toInt(0);
51 if (scaleOrStrengthKey)
52 info.scaleOrStrength = float(object.value(QLatin1String(scaleOrStrengthKey)).toDouble(1.0));
53
54 const QJsonObject extensions = object.value(QLatin1String("extensions")).toObject();
55 const QJsonValue transformValue = extensions.value(QLatin1String("KHR_texture_transform"));
56 if (transformValue.isObject()) {
57 const QJsonObject t = transformValue.toObject();
58 TextureTransform transform;
59 const QJsonArray offset = t.value(QLatin1String("offset")).toArray();
60 if (offset.size() >= 2)
61 transform.offset = QVector2D(float(offset.at(0).toDouble()), float(offset.at(1).toDouble()));
62 const QJsonArray scale = t.value(QLatin1String("scale")).toArray();
63 if (scale.size() >= 2)
64 transform.scale = QVector2D(float(scale.at(0).toDouble(1.0)), float(scale.at(1).toDouble(1.0)));
65 transform.rotation = float(t.value(QLatin1String("rotation")).toDouble(0.0));
66 transform.texCoord = t.value(QLatin1String("texCoord")).toInt(-1);
67 info.transform = transform;
68 }
69 return info;
70}
71
72Accessor::Type accessorTypeFromString(const QString &type, bool *ok)
73{
74 *ok = true;
75 if (type == QLatin1String("SCALAR"))
76 return Accessor::Type::Scalar;
77 if (type == QLatin1String("VEC2"))
78 return Accessor::Type::Vec2;
79 if (type == QLatin1String("VEC3"))
80 return Accessor::Type::Vec3;
81 if (type == QLatin1String("VEC4"))
82 return Accessor::Type::Vec4;
83 if (type == QLatin1String("MAT2"))
84 return Accessor::Type::Mat2;
85 if (type == QLatin1String("MAT3"))
86 return Accessor::Type::Mat3;
87 if (type == QLatin1String("MAT4"))
88 return Accessor::Type::Mat4;
89 *ok = false;
90 return Accessor::Type::Scalar;
91}
92
93bool isValidComponentType(int value)
94{
95 switch (Accessor::ComponentType(value)) {
96 case Accessor::ComponentType::Byte:
97 case Accessor::ComponentType::UnsignedByte:
98 case Accessor::ComponentType::Short:
99 case Accessor::ComponentType::UnsignedShort:
100 case Accessor::ComponentType::UnsignedInt:
101 case Accessor::ComponentType::Float:
102 return true;
103 }
104 return false;
105}
106
107} // namespace
108
109/*!
110 \class QSSGGltfParser
111 \internal
112
113 Parses glTF 2.0 content (.gltf JSON or binary .glb container) into a
114 QSSGGltfDocument. Buffers (external files, data: URIs, and the GLB BIN
115 chunk) are resolved eagerly; image contents are left unresolved.
116
117 Validation is two-tier: structural violations (out-of-range indices,
118 accessors extending past their buffer view, truncated GLB, unsupported
119 required extensions) fail the parse with an error message; tolerable
120 deviations from the specification are logged as warnings and clamped,
121 repaired, or ignored, matching the leniency of other glTF consumers. A
122 node with more than one parent and a scene root that is not unique both
123 fall in the second group: the offending edge is dropped, because the node
124 hierarchy has to be a forest for consumers to recurse over it safely, but
125 the rest of the asset is still worth loading.
126*/
127
128/*!
129 \internal
130
131 Returns the extensions the parser understands. Assets that list anything
132 else in extensionsRequired fail to parse.
133
134 Being on this list means the extension is represented in QSSGGltfDocument,
135 not that any particular consumer acts on it: a consumer that ignores one
136 of these still gets a document it can load.
137*/
138QStringList QSSGGltfParser::supportedExtensions()
139{
140 // Order does not matter; keep alphabetical for readability. Extensions
141 // needing a decoder we do not ship (KHR_draco_mesh_compression,
142 // KHR_texture_basisu) are intentionally absent and get a clear error
143 // when required by an asset.
144 return {
145 QStringLiteral("EXT_mesh_gpu_instancing"),
146 QStringLiteral("KHR_lights_punctual"),
147 QStringLiteral("KHR_materials_clearcoat"),
148 QStringLiteral("KHR_materials_emissive_strength"),
149 QStringLiteral("KHR_materials_ior"),
150 QStringLiteral("KHR_materials_pbrSpecularGlossiness"),
151 QStringLiteral("KHR_materials_specular"),
152 QStringLiteral("KHR_materials_transmission"),
153 QStringLiteral("KHR_materials_unlit"),
154 QStringLiteral("KHR_materials_variants"),
155 QStringLiteral("KHR_materials_volume"),
156 QStringLiteral("KHR_mesh_quantization"),
157 QStringLiteral("KHR_texture_transform"),
158 };
159}
160
161bool QSSGGltfParser::setError(const QString &message)
162{
163 m_errorMessage = message;
164 qCWarning(lcQuick3DGltf) << message;
165 return false;
166}
167
168/*!
169 \internal
170
171 Convenience overload of parse() reading \a filePath, a local file or qrc
172 path, into \a document.
173*/
174bool QSSGGltfParser::parseFile(const QString &filePath, QSSGGltfDocument *document)
175{
176 QFile file(filePath);
177 if (!file.open(QIODevice::ReadOnly))
178 return setError(QStringLiteral("Failed to open '%1': %2").arg(filePath, file.errorString()));
179 return parse(file.readAll(), QFileInfo(filePath).path(), document);
180}
181
182/*!
183 \internal
184
185 Parses .gltf JSON or a .glb container in \a data into \a document.
186 \a baseDir is the local or qrc directory used to resolve relative
187 buffer and image URIs.
188*/
189bool QSSGGltfParser::parse(const QByteArray &data, const QString &baseDir, QSSGGltfDocument *document)
190{
191 Q_ASSERT(document);
192 m_errorMessage.clear();
193 *document = QSSGGltfDocument();
194 document->baseDir = baseDir;
195
196 QByteArray json = data;
197 QByteArray binChunk;
198
199 // Binary container? (12-byte header: magic, version, length)
200 if (data.size() >= 12 && qFromLittleEndian<quint32>(data.constData()) == GLB_MAGIC) {
201 const quint32 version = qFromLittleEndian<quint32>(data.constData() + 4);
202 if (version != 2)
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()));
208
209 json.clear();
210 qint64 offset = 12;
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);
214 offset += 8;
215 if (offset + qint64(chunkLength) > qint64(length))
216 return setError(QStringLiteral("Truncated GLB chunk at offset %1").arg(offset - 8));
217
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);
222 else
223 qCWarning(lcQuick3DGltf) << "Skipping unknown GLB chunk type" << Qt::hex << chunkType;
224
225 // Chunks are 4-byte aligned
226 offset += chunkLength;
227 if (offset % 4)
228 offset += 4 - (offset % 4);
229 }
230 if (json.isEmpty())
231 return setError(QStringLiteral("GLB container has no JSON chunk"));
232 }
233
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"));
240
241 const QJsonObject root = jsonDocument.object();
242
243 // asset (required)
244 {
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();
253
254 const int major = document->asset.version.section(QLatin1Char('.'), 0, 0).toInt();
255 if (major != 2)
256 return setError(QStringLiteral("Unsupported glTF version '%1'").arg(document->asset.version));
257
258 // The specification requires a client to fail when minVersion asks for
259 // more than it implements. Only 2.0 exists so far, so this is about not
260 // silently loading a future asset that says it needs more.
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));
267 }
268 }
269
270 // extensionsUsed / extensionsRequired
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();
276
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));
281 }
282 for (const QString &used : std::as_const(document->extensionsUsed)) {
283 if (!supported.contains(used))
284 qCWarning(lcQuick3DGltf) << "Ignoring unsupported glTF extension" << used;
285 }
286
287 // buffers
288 for (const auto &value : root.value(QLatin1String("buffers")).toArray()) {
289 const QJsonObject object = value.toObject();
290 Buffer buffer;
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()) {
296 // GLB BIN chunk; only valid for the first buffer
297 if (document->buffers.isEmpty() && !binChunk.isEmpty())
298 buffer.data = binChunk;
299 else if (binChunk.isEmpty())
300 return setError(
301 QStringLiteral("Buffer %1 has no URI and there is no GLB BIN chunk")
302 .arg(document->buffers.size()));
303 else
304 return setError(QStringLiteral("Only the first buffer may refer to the GLB BIN chunk"));
305 } else {
306 QString resolveError;
307 buffer.data = QSSGGltfResourceResolver::loadUri(buffer.uri, baseDir, &resolveError);
308 // Loading can legitimately produce nothing, for an empty file or an
309 // empty data URI payload, so the error string is what says whether
310 // it failed. The length check below rejects a short buffer anyway.
311 if (!resolveError.isEmpty())
312 return setError(QStringLiteral("Failed to load buffer %1: %2")
313 .arg(document->buffers.size()).arg(resolveError));
314 }
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);
319 }
320
321 // bufferViews
322 for (const auto &value : root.value(QLatin1String("bufferViews")).toArray()) {
323 const QJsonObject object = value.toObject();
324 BufferView view;
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();
331
332 if (view.buffer < 0 || view.buffer >= document->buffers.size())
333 return setError(
334 QStringLiteral("Buffer view %1 references invalid buffer %2")
335 .arg(document->bufferViews.size()).arg(view.buffer));
336 // The stride bound is from the specification; together with
337 // non-negative offsets and lengths it also keeps all later offset
338 // arithmetic far away from overflowing 64 bits.
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()));
342 // Compared without adding the two together: both are individually
343 // positive by the check above, but their sum can overflow to a negative
344 // value and slip under the buffer length.
345 const qint64 bufferLength = document->buffers.at(view.buffer).byteLength;
346 if (view.byteLength > bufferLength || view.byteOffset > bufferLength - view.byteLength)
347 return setError(
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);
351 }
352
353 // accessors
354 for (const auto &value : root.value(QLatin1String("accessors")).toArray()) {
355 const QJsonObject object = value.toObject();
356 Accessor accessor;
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);
364 bool typeOk = false;
365 accessor.type = accessorTypeFromString(object.value(QLatin1String("type")).toString(), &typeOk);
366 if (!typeOk)
367 return setError(QStringLiteral("Accessor %1 has invalid type '%2'")
368 .arg(document->accessors.size())
369 .arg(object.value(QLatin1String("type")).toString()));
370 // The specification puts no upper bound on count, but the offset
371 // arithmetic below needs one. A count near the top of the qint64 range
372 // makes those products overflow, which is undefined behavior, and in
373 // practice wraps the range test at the end of this loop into an
374 // acceptance: a ten-line document then yields an accessor claiming
375 // more elements than could ever be allocated. One element occupies at
376 // least one byte, so no real asset comes anywhere near this cap, and
377 // capping in elements keeps every product here well inside 64 bits.
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());
393
394 const QJsonValue sparseValue = object.value(QLatin1String("sparse"));
395 if (sparseValue.isObject()) {
396 const QJsonObject sparseObject = sparseValue.toObject();
397 Accessor::Sparse sparse;
398 // Bounded the same way as accessor.count, and for the same reason:
399 // it is multiplied by a component size a few lines down
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))
410 return setError(
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));
417
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()));
422 }
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()));
427 }
428 // Offsets kept out of the sums, like the buffer view check above
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) {
438 return setError(
439 QStringLiteral("Accessor %1 sparse data extends past the end of its buffer view")
440 .arg(document->accessors.size()));
441 }
442 accessor.sparse = sparse;
443 }
444
445 if (accessor.bufferView >= document->bufferViews.size())
446 return setError(
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;
453 // Offset kept out of the sum, like the buffer view check above
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));
460 } else {
461 // An accessor without a buffer view is read as all zeros, so no
462 // buffer bounds its size. The element cap above keeps this product
463 // from overflowing, but 256M elements of MAT4 would still be a
464 // 16 GB allocation, so bound the byte count as well.
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()));
469 }
470 document->accessors.append(accessor);
471 }
472
473 // images
474 for (const auto &value : root.value(QLatin1String("images")).toArray()) {
475 const QJsonObject object = value.toObject();
476 Image image;
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);
485 }
486
487 // samplers
488 for (const auto &value : root.value(QLatin1String("samplers")).toArray()) {
489 const QJsonObject object = value.toObject();
490 Sampler sampler;
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);
497 }
498
499 // textures
500 for (const auto &value : root.value(QLatin1String("textures")).toArray()) {
501 const QJsonObject object = value.toObject();
502 Texture texture;
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);
514 }
515
516 // materials
517 //
518 // Every texture reference goes through parseTexture() rather than
519 // parseTextureInfo() directly, so that the index is range checked in one
520 // place and a newly supported extension cannot forget to do it. The
521 // textures array is already parsed at this point.
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;
528 return info;
529 };
530
531 for (const auto &value : root.value(QLatin1String("materials")).toArray()) {
532 const QJsonObject object = value.toObject();
533 Material material;
534 material.name = object.value(QLatin1String("name")).toString();
535
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());
546 }
547
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);
554
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);
562
563 material.extensions = object.value(QLatin1String("extensions")).toObject();
564 const QJsonObject &ext = material.extensions;
565
566 material.unlit = ext.contains(QLatin1String("KHR_materials_unlit"));
567
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;
581 }
582
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;
596 }
597
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;
606 }
607
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;
618 }
619
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));
623
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));
628
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;
640 }
641
642 document->materials.append(material);
643 }
644
645 if (badTextureIndex >= 0)
646 return setError(QStringLiteral("Material references invalid texture %1").arg(badTextureIndex));
647
648 // meshes
649 for (const auto &value : root.value(QLatin1String("meshes")).toArray()) {
650 const QJsonObject object = value.toObject();
651 Mesh mesh;
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()));
655
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);
671 }
672 primitive.extensions = primitiveObject.value(QLatin1String("extensions")).toObject();
673
674 // Structural validation of accessor references. As everywhere in
675 // this parser, any negative index means "unset": the glTF defaults
676 // are -1, the readers return nothing for a negative index, and so
677 // only the upper bound needs checking here.
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));
682 return true;
683 };
684 for (auto it = primitive.attributes.constBegin(); it != primitive.attributes.constEnd(); ++it) {
685 if (!checkAccessor(it.value(), it.key().constData()))
686 return false;
687 }
688 if (primitive.indices >= 0 && !checkAccessor(primitive.indices, "index"))
689 return false;
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));
694
695 mesh.primitives.append(primitive);
696 }
697 if (mesh.primitives.isEmpty())
698 qCWarning(lcQuick3DGltf) << "Mesh" << document->meshes.size() << "has no primitives";
699 document->meshes.append(mesh);
700 }
701
702 // cameras
703 for (const auto &value : root.value(QLatin1String("cameras")).toArray()) {
704 const QJsonObject object = value.toObject();
705 Camera camera;
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());
715 } else {
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));
722 }
723 document->cameras.append(camera);
724 }
725
726 // KHR_lights_punctual (document level)
727 {
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();
732 Light light;
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;
739 else
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));
748 }
749 document->lights.append(light);
750 }
751 }
752 }
753
754 // nodes
755 for (const auto &value : root.value(QLatin1String("nodes")).toArray()) {
756 const QJsonObject object = value.toObject();
757 Node node;
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);
764
765 const QJsonValue matrixValue = object.value(QLatin1String("matrix"));
766 if (matrixValue.isArray()) {
767 const QJsonArray m = matrixValue.toArray();
768 if (m.size() == 16) {
769 float values[16];
770 for (int i = 0; i < 16; ++i)
771 values[i] = float(m.at(i).toDouble());
772 // glTF matrices are column-major; QMatrix4x4(float*) is row-major
773 node.matrix = QMatrix4x4(values).transposed();
774 node.hasMatrix = true;
775 }
776 }
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) {
780 // glTF quaternions are (x, y, z, w)
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()));
783 }
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();
788
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);
792
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);
803 }
804
805 // Validate node graph references (children and skins can be forward
806 // references). The specification requires the hierarchy to be a forest,
807 // and consumers that recurse over the graph rely on that to be safe from
808 // cycles and exponential blowup, so the shape is repaired here rather than
809 // left for them to cope with.
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) {
820 // A second parent cannot be represented at all: a node
821 // occupies exactly one place in a scene graph. Keep the first
822 // edge and drop this one, so the rest of the asset still
823 // loads.
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--);
828 }
829 }
830 }
831 // With single parents the only remaining hazards are parentless cycles
832 // and excessive depth; both are caught by walking up each node's parent
833 // chain, memoizing the depths so every edge is visited only once.
834 {
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)
840 parent[child] = i;
841 }
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) {
846 chain.clear();
847 int n = i;
848 while (n >= 0 && depth[n] < 0 && chain.size() <= nodeCount) {
849 chain.append(n);
850 n = parent[n];
851 }
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)
856 depth[*it] = ++d;
857 if (d > maxNodeDepth)
858 return setError(QStringLiteral("Node hierarchy is deeper than %1").arg(maxNodeDepth));
859 }
860 }
861
862 // skins
863 for (const auto &value : root.value(QLatin1String("skins")).toArray()) {
864 const QJsonObject object = value.toObject();
865 Skin skin;
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));
871
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));
879 }
880 document->skins.append(skin);
881 }
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));
886 }
887
888 // animations
889 for (const auto &value : root.value(QLatin1String("animations")).toArray()) {
890 const QJsonObject object = value.toObject();
891 Animation animation;
892 animation.name = object.value(QLatin1String("name")).toString();
893
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()) {
906 return setError(
907 QStringLiteral("Animation %1 sampler references an invalid accessor")
908 .arg(document->animations.size()));
909 }
910 animation.samplers.append(sampler);
911 }
912
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;
928 continue;
929 }
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);
939 }
940
941 document->animations.append(animation);
942 }
943
944 // scenes
945 for (const auto &value : root.value(QLatin1String("scenes")).toArray()) {
946 const QJsonObject object = value.toObject();
947 Scene scene;
948 scene.name = object.value(QLatin1String("name")).toString();
949 QSet<int> seenRoots;
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));
955 // Scene nodes must be distinct root nodes; a repeated node, or one
956 // that is also somebody's child, would have its subtree emitted
957 // more than once. Skip the offending entry rather than failing, so
958 // that the scene still contains everything it should, once.
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";
962 continue;
963 }
964 seenRoots.insert(index);
965 scene.nodes.append(index);
966 }
967 document->scenes.append(scene);
968 }
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));
972
973 return true;
974}
975
976QT_END_NAMESPACE
Combined button and popup list for selecting options.