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/qhash.h>
12#include <QtCore/qjsonarray.h>
13#include <QtCore/qjsondocument.h>
14#include <QtCore/qset.h>
15#include <QtCore/qvarlengtharray.h>
16
17#include "meshoptimizer.h"
18
20
21Q_LOGGING_CATEGORY(lcQuick3DGltf, "qt.quick3d.gltf")
22
23using namespace QSSGGltf;
24
25namespace {
26
27// GLB container constants
28constexpr quint32 GLB_MAGIC = 0x46546C67; // 'glTF'
29constexpr quint32 GLB_CHUNK_JSON = 0x4E4F534A; // 'JSON'
30constexpr quint32 GLB_CHUNK_BIN = 0x004E4942; // 'BIN\0'
31
32QVector3D toVector3D(const QJsonArray &array, const QVector3D &defaultValue = {})
33{
34 if (array.size() < 3)
35 return defaultValue;
36 return QVector3D(float(array.at(0).toDouble()), float(array.at(1).toDouble()), float(array.at(2).toDouble()));
37}
38
39QVector4D toVector4D(const QJsonArray &array, const QVector4D &defaultValue = {})
40{
41 if (array.size() < 4)
42 return defaultValue;
43 return QVector4D(float(array.at(0).toDouble()), float(array.at(1).toDouble()),
44 float(array.at(2).toDouble()), float(array.at(3).toDouble()));
45}
46
47TextureInfo parseTextureInfo(const QJsonObject &object, const char *scaleOrStrengthKey = nullptr)
48{
49 TextureInfo info;
50 if (object.isEmpty())
51 return info;
52 info.index = object.value(QLatin1String("index")).toInt(-1);
53 info.texCoord = object.value(QLatin1String("texCoord")).toInt(0);
54 if (scaleOrStrengthKey)
55 info.scaleOrStrength = float(object.value(QLatin1String(scaleOrStrengthKey)).toDouble(1.0));
56
57 const QJsonObject extensions = object.value(QLatin1String("extensions")).toObject();
58 const QJsonValue transformValue = extensions.value(QLatin1String("KHR_texture_transform"));
59 if (transformValue.isObject()) {
60 const QJsonObject t = transformValue.toObject();
61 TextureTransform transform;
62 const QJsonArray offset = t.value(QLatin1String("offset")).toArray();
63 if (offset.size() >= 2)
64 transform.offset = QVector2D(float(offset.at(0).toDouble()), float(offset.at(1).toDouble()));
65 const QJsonArray scale = t.value(QLatin1String("scale")).toArray();
66 if (scale.size() >= 2)
67 transform.scale = QVector2D(float(scale.at(0).toDouble(1.0)), float(scale.at(1).toDouble(1.0)));
68 transform.rotation = float(t.value(QLatin1String("rotation")).toDouble(0.0));
69 transform.texCoord = t.value(QLatin1String("texCoord")).toInt(-1);
70 info.transform = transform;
71 }
72 return info;
73}
74
75Accessor::Type accessorTypeFromString(const QString &type, bool *ok)
76{
77 *ok = true;
78 if (type == QLatin1String("SCALAR"))
79 return Accessor::Type::Scalar;
80 if (type == QLatin1String("VEC2"))
81 return Accessor::Type::Vec2;
82 if (type == QLatin1String("VEC3"))
83 return Accessor::Type::Vec3;
84 if (type == QLatin1String("VEC4"))
85 return Accessor::Type::Vec4;
86 if (type == QLatin1String("MAT2"))
87 return Accessor::Type::Mat2;
88 if (type == QLatin1String("MAT3"))
89 return Accessor::Type::Mat3;
90 if (type == QLatin1String("MAT4"))
91 return Accessor::Type::Mat4;
92 *ok = false;
93 return Accessor::Type::Scalar;
94}
95
96bool isValidComponentType(int value)
97{
98 switch (Accessor::ComponentType(value)) {
99 case Accessor::ComponentType::Byte:
100 case Accessor::ComponentType::UnsignedByte:
101 case Accessor::ComponentType::Short:
102 case Accessor::ComponentType::UnsignedShort:
103 case Accessor::ComponentType::UnsignedInt:
104 case Accessor::ComponentType::Float:
105 return true;
106 }
107 return false;
108}
109
110// Views can alias the same compressed bytes, so the budget is per document
111struct MeshoptDecodeState
112{
113 qint64 remainingBytes = qint64(1024) * 1024 * 1024;
114 QHash<QString, int> decodedBuffers; // decode parameters -> synthetic buffer index
115};
116
117// Returns -1 for anything but a non-negative integer below 2^53
118qint64 toByteCount(const QJsonValue &value, double defaultValue)
119{
120 const double d = value.toDouble(defaultValue);
121 return d >= 0.0 && d <= 9007199254740992.0 ? qint64(d) : -1;
122}
123
124// Decodes into a synthetic buffer and points the view at it
125bool decodeMeshoptBufferView(const QJsonObject &ext, QSSGGltfDocument &document, BufferView &view,
126 MeshoptDecodeState &state, QString *errorMessage)
127{
128 const auto fail = [errorMessage](const QString &message) {
129 *errorMessage = message;
130 return false;
131 };
132
133 const int sourceBuffer = ext.value(QLatin1String("buffer")).toInt(-1);
134 const qint64 byteOffset = toByteCount(ext.value(QLatin1String("byteOffset")), 0.0);
135 const qint64 byteLength = toByteCount(ext.value(QLatin1String("byteLength")), -1.0);
136 const qint64 byteStride = toByteCount(ext.value(QLatin1String("byteStride")), -1.0);
137
138 // Checked as double, since the qint64 product can wrap
139 constexpr qint64 maxDecodedBytes = qint64(1024) * 1024 * 1024;
140 const double rawCount = ext.value(QLatin1String("count")).toDouble();
141 if (!(byteStride > 0 && rawCount > 0.0 && rawCount * double(byteStride) <= double(maxDecodedBytes)))
142 return fail(QStringLiteral("unreasonable decoded data size"));
143 const qint64 count = qint64(rawCount);
144
145 const QString mode = ext.value(QLatin1String("mode")).toString();
146 const QString filter = ext.value(QLatin1String("filter")).toString(QStringLiteral("NONE"));
147
148 if (sourceBuffer < 0 || sourceBuffer >= document.buffers.size()
149 || document.buffers.at(sourceBuffer).meshoptFallback) {
150 return fail(QStringLiteral("compressed data references invalid buffer %1").arg(sourceBuffer));
151 }
152 const Buffer &compressed = document.buffers.at(sourceBuffer);
153 // Not summed, since offset + length can wrap
154 if (byteOffset < 0 || byteLength <= 0 || byteLength > compressed.byteLength
155 || byteOffset > compressed.byteLength - byteLength) {
156 return fail(QStringLiteral("compressed data is outside buffer %1").arg(sourceBuffer));
157 }
158
159 enum class Mode { Attributes, Triangles, Indices };
160 Mode decodeMode;
161 if (mode == QLatin1String("ATTRIBUTES"))
162 decodeMode = Mode::Attributes;
163 else if (mode == QLatin1String("TRIANGLES"))
164 decodeMode = Mode::Triangles;
165 else if (mode == QLatin1String("INDICES"))
166 decodeMode = Mode::Indices;
167 else
168 return fail(QStringLiteral("unsupported compression mode '%1'").arg(mode));
169
170 // Stride limits required by the decoders
171 if (decodeMode == Mode::Attributes) {
172 if (byteStride < 4 || byteStride > 256 || byteStride % 4)
173 return fail(QStringLiteral("invalid byte stride %1 for ATTRIBUTES mode").arg(byteStride));
174 } else {
175 if (byteStride != 2 && byteStride != 4)
176 return fail(QStringLiteral("invalid byte stride %1 for index data").arg(byteStride));
177 if (decodeMode == Mode::Triangles && count % 3)
178 return fail(QStringLiteral("TRIANGLES element count %1 is not divisible by 3").arg(count));
179 if (filter != QLatin1String("NONE"))
180 return fail(QStringLiteral("filters do not apply to index data"));
181 }
182
183 if (decodeMode == Mode::Attributes && filter != QLatin1String("NONE") && filter != QLatin1String("OCTAHEDRAL")
184 && filter != QLatin1String("QUATERNION") && filter != QLatin1String("EXPONENTIAL")) {
185 return fail(QStringLiteral("unsupported filter '%1'").arg(filter));
186 }
187 if ((filter == QLatin1String("OCTAHEDRAL") && byteStride != 4 && byteStride != 8)
188 || (filter == QLatin1String("QUATERNION") && byteStride != 8)) {
189 return fail(QStringLiteral("invalid byte stride %1 for filter %2").arg(byteStride).arg(filter));
190 }
191
192 const QString key = QStringLiteral("%1:%2:%3:%4:%5:%6:%7")
193 .arg(sourceBuffer).arg(byteOffset).arg(byteLength).arg(count)
194 .arg(byteStride).arg(mode, filter);
195 const auto rewriteView = [&view, decodeMode, byteStride](int buffer, qint64 length) {
196 view.buffer = buffer;
197 view.byteOffset = 0;
198 view.byteLength = length;
199 view.byteStride = decodeMode == Mode::Attributes ? int(byteStride) : 0;
200 };
201 if (const auto it = state.decodedBuffers.constFind(key); it != state.decodedBuffers.constEnd()) {
202 rewriteView(*it, document.buffers.at(*it).byteLength);
203 return true;
204 }
205 if (count * byteStride > state.remainingBytes)
206 return fail(QStringLiteral("decoded data exceeds the limit for the document"));
207
208 QByteArray decoded(count * byteStride, Qt::Uninitialized);
209 const auto *source = reinterpret_cast<const unsigned char *>(compressed.data.constData()) + byteOffset;
210 int result = -1;
211 switch (decodeMode) {
212 case Mode::Attributes:
213 result = meshopt_decodeVertexBuffer(decoded.data(), size_t(count), size_t(byteStride), source,
214 size_t(byteLength));
215 break;
216 case Mode::Triangles:
217 result = meshopt_decodeIndexBuffer(decoded.data(), size_t(count), size_t(byteStride), source,
218 size_t(byteLength));
219 break;
220 case Mode::Indices:
221 result = meshopt_decodeIndexSequence(decoded.data(), size_t(count), size_t(byteStride), source,
222 size_t(byteLength));
223 break;
224 }
225 if (result != 0)
226 return fail(QStringLiteral("corrupt compressed data"));
227
228 if (filter == QLatin1String("OCTAHEDRAL"))
229 meshopt_decodeFilterOct(decoded.data(), size_t(count), size_t(byteStride));
230 else if (filter == QLatin1String("QUATERNION"))
231 meshopt_decodeFilterQuat(decoded.data(), size_t(count), size_t(byteStride));
232 else if (filter == QLatin1String("EXPONENTIAL"))
233 meshopt_decodeFilterExp(decoded.data(), size_t(count), size_t(byteStride));
234
235 state.remainingBytes -= decoded.size();
236 Buffer decodedBuffer;
237 decodedBuffer.byteLength = decoded.size();
238 decodedBuffer.data = std::move(decoded);
239 document.buffers.append(decodedBuffer);
240 const int bufferIndex = int(document.buffers.size() - 1);
241 state.decodedBuffers.insert(key, bufferIndex);
242 rewriteView(bufferIndex, decodedBuffer.byteLength);
243 return true;
244}
245
246} // namespace
247
248/*!
249 \class QSSGGltfParser
250 \internal
251
252 Parses glTF 2.0 content (.gltf JSON or binary .glb container) into a
253 QSSGGltfDocument. Buffers (external files, data: URIs, and the GLB BIN
254 chunk) are resolved eagerly; image contents are left unresolved.
255
256 Validation is two-tier: structural violations (out-of-range indices,
257 accessors extending past their buffer view, truncated GLB, unsupported
258 required extensions) fail the parse with an error message; tolerable
259 deviations from the specification are logged as warnings and clamped,
260 repaired, or ignored, matching the leniency of other glTF consumers. A
261 node with more than one parent and a scene root that is not unique both
262 fall in the second group: the offending edge is dropped, because the node
263 hierarchy has to be a forest for consumers to recurse over it safely, but
264 the rest of the asset is still worth loading.
265*/
266
267/*!
268 \internal
269
270 Returns the extensions the parser understands. Assets that list anything
271 else in extensionsRequired fail to parse.
272
273 Being on this list means the extension is represented in QSSGGltfDocument,
274 not that any particular consumer acts on it: a consumer that ignores one
275 of these still gets a document it can load.
276*/
277QStringList QSSGGltfParser::supportedExtensions()
278{
279 // Order does not matter; keep alphabetical for readability. Extensions
280 // needing a decoder we do not ship (KHR_draco_mesh_compression,
281 // KHR_texture_basisu) are intentionally absent and get a clear error
282 // when required by an asset.
283 // KHR_xmp_json_ld is accepted, but left in the raw extension objects.
284 return {
285 QStringLiteral("EXT_mesh_gpu_instancing"),
286 QStringLiteral("EXT_meshopt_compression"),
287 QStringLiteral("EXT_texture_webp"),
288 QStringLiteral("KHR_lights_punctual"),
289 QStringLiteral("KHR_materials_anisotropy"),
290 QStringLiteral("KHR_materials_clearcoat"),
291 QStringLiteral("KHR_materials_dispersion"),
292 QStringLiteral("KHR_materials_emissive_strength"),
293 QStringLiteral("KHR_materials_ior"),
294 QStringLiteral("KHR_materials_iridescence"),
295 QStringLiteral("KHR_materials_pbrSpecularGlossiness"),
296 QStringLiteral("KHR_materials_sheen"),
297 QStringLiteral("KHR_materials_specular"),
298 QStringLiteral("KHR_materials_transmission"),
299 QStringLiteral("KHR_materials_unlit"),
300 QStringLiteral("KHR_materials_variants"),
301 QStringLiteral("KHR_materials_volume"),
302 QStringLiteral("KHR_mesh_quantization"),
303 QStringLiteral("KHR_node_visibility"),
304 QStringLiteral("KHR_texture_transform"),
305 QStringLiteral("KHR_xmp_json_ld"),
306 };
307}
308
309bool QSSGGltfParser::setError(const QString &message)
310{
311 m_errorMessage = message;
312 qCWarning(lcQuick3DGltf) << message;
313 return false;
314}
315
316/*!
317 \internal
318
319 Convenience overload of parse() reading \a filePath, a local file or qrc
320 path, into \a document.
321*/
322bool QSSGGltfParser::parseFile(const QString &filePath, QSSGGltfDocument *document)
323{
324 QFile file(filePath);
325 if (!file.open(QIODevice::ReadOnly))
326 return setError(QStringLiteral("Failed to open '%1': %2").arg(filePath, file.errorString()));
327 return parse(file.readAll(), QFileInfo(filePath).path(), document);
328}
329
330/*!
331 \internal
332
333 Parses .gltf JSON or a .glb container in \a data into \a document.
334 \a baseDir is the local or qrc directory used to resolve relative
335 buffer and image URIs.
336*/
337bool QSSGGltfParser::parse(const QByteArray &data, const QString &baseDir, QSSGGltfDocument *document)
338{
339 Q_ASSERT(document);
340 m_errorMessage.clear();
341 *document = QSSGGltfDocument();
342 document->baseDir = baseDir;
343
344 QByteArray json = data;
345 QByteArray binChunk;
346
347 // Binary container? (12-byte header: magic, version, length)
348 if (data.size() >= 12 && qFromLittleEndian<quint32>(data.constData()) == GLB_MAGIC) {
349 const quint32 version = qFromLittleEndian<quint32>(data.constData() + 4);
350 if (version != 2)
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()));
356
357 json.clear();
358 qint64 offset = 12;
359 while (offset + 8 <= qint64(length)) {
360 QT_WARNING_PUSH QT_WARNING_DISABLE_GCC("-Warray-bounds")
361 // gcc 15.2.0 warns because of static const char _empty in QByteArray
362 const quint32 chunkLength = qFromLittleEndian<quint32>(data.constData() + offset);
363 const quint32 chunkType = qFromLittleEndian<quint32>(data.constData() + offset + 4);
364 QT_WARNING_POP
365 offset += 8;
366 if (offset + qint64(chunkLength) > qint64(length))
367 return setError(QStringLiteral("Truncated GLB chunk at offset %1").arg(offset - 8));
368
369 if (chunkType == GLB_CHUNK_JSON && json.isEmpty())
370 json = data.mid(offset, chunkLength);
371 else if (chunkType == GLB_CHUNK_BIN && binChunk.isEmpty())
372 binChunk = data.mid(offset, chunkLength);
373 else
374 qCWarning(lcQuick3DGltf) << "Skipping unknown GLB chunk type" << Qt::hex << chunkType;
375
376 // Chunks are 4-byte aligned
377 offset += chunkLength;
378 if (offset % 4)
379 offset += 4 - (offset % 4);
380 }
381 if (json.isEmpty())
382 return setError(QStringLiteral("GLB container has no JSON chunk"));
383 }
384
385 QJsonParseError jsonError;
386 const QJsonDocument jsonDocument = QJsonDocument::fromJson(json, &jsonError);
387 if (jsonDocument.isNull())
388 return setError(QStringLiteral("Invalid glTF JSON: %1").arg(jsonError.errorString()));
389 if (!jsonDocument.isObject())
390 return setError(QStringLiteral("Invalid glTF JSON: root is not an object"));
391
392 const QJsonObject root = jsonDocument.object();
393
394 // asset (required)
395 {
396 const QJsonValue assetValue = root.value(QLatin1String("asset"));
397 if (!assetValue.isObject())
398 return setError(QStringLiteral("Not a glTF document: no asset object"));
399 const QJsonObject asset = assetValue.toObject();
400 document->asset.version = asset.value(QLatin1String("version")).toString();
401 document->asset.minVersion = asset.value(QLatin1String("minVersion")).toString();
402 document->asset.generator = asset.value(QLatin1String("generator")).toString();
403 document->asset.copyright = asset.value(QLatin1String("copyright")).toString();
404
405 const int major = document->asset.version.section(QLatin1Char('.'), 0, 0).toInt();
406 if (major != 2)
407 return setError(QStringLiteral("Unsupported glTF version '%1'").arg(document->asset.version));
408
409 // The specification requires a client to fail when minVersion asks for
410 // more than it implements. Only 2.0 exists so far, so this is about not
411 // silently loading a future asset that says it needs more.
412 if (!document->asset.minVersion.isEmpty()) {
413 const int minMajor = document->asset.minVersion.section(QLatin1Char('.'), 0, 0).toInt();
414 const int minMinor = document->asset.minVersion.section(QLatin1Char('.'), 1, 1).toInt();
415 if (minMajor != 2 || minMinor > 0)
416 return setError(QStringLiteral("Asset requires glTF version '%1' or higher")
417 .arg(document->asset.minVersion));
418 }
419 }
420
421 // extensionsUsed / extensionsRequired
422 const QJsonArray extensionsUsed = root.value(QLatin1String("extensionsUsed")).toArray();
423 for (const auto &value : extensionsUsed)
424 document->extensionsUsed.append(value.toString());
425 const QJsonArray extensionsRequired = root.value(QLatin1String("extensionsRequired")).toArray();
426 for (const auto &value : extensionsRequired)
427 document->extensionsRequired.append(value.toString());
428 document->rootExtensions = root.value(QLatin1String("extensions")).toObject();
429
430 // KHR_materials_variants (document level): the variant names
431 {
432 const QJsonValue variantsExt = document->rootExtensions.value(QLatin1String("KHR_materials_variants"));
433 if (variantsExt.isObject()) {
434 const QJsonArray variants = variantsExt.toObject().value(QLatin1String("variants")).toArray();
435 for (const auto &value : variants)
436 document->materialVariants.append(value.toObject().value(QLatin1String("name")).toString());
437 }
438 }
439
440 const QStringList supported = supportedExtensions();
441 for (const QString &required : std::as_const(document->extensionsRequired)) {
442 if (!supported.contains(required))
443 return setError(QStringLiteral("Asset requires unsupported glTF extension '%1'").arg(required));
444 }
445 for (const QString &used : std::as_const(document->extensionsUsed)) {
446 if (!supported.contains(used))
447 qCWarning(lcQuick3DGltf) << "Ignoring unsupported glTF extension" << used;
448 }
449
450 // buffers
451 const QJsonArray buffers = root.value(QLatin1String("buffers")).toArray();
452 for (const auto &value : buffers) {
453 const QJsonObject object = value.toObject();
454 Buffer buffer;
455 buffer.uri = object.value(QLatin1String("uri")).toString();
456 buffer.byteLength = qint64(object.value(QLatin1String("byteLength")).toDouble());
457 if (buffer.byteLength < 0)
458 return setError(QStringLiteral("Buffer %1 has negative byte length").arg(document->buffers.size()));
459 buffer.meshoptFallback = object.value(QLatin1String("extensions")).toObject()
460 .value(QLatin1String("EXT_meshopt_compression")).toObject()
461 .value(QLatin1String("fallback")).toBool(false);
462 if (buffer.meshoptFallback) {
463 document->buffers.append(buffer);
464 continue;
465 }
466 if (buffer.uri.isEmpty()) {
467 // GLB BIN chunk; only valid for the first buffer
468 if (document->buffers.isEmpty() && !binChunk.isEmpty())
469 buffer.data = binChunk;
470 else if (binChunk.isEmpty())
471 return setError(
472 QStringLiteral("Buffer %1 has no URI and there is no GLB BIN chunk")
473 .arg(document->buffers.size()));
474 else
475 return setError(QStringLiteral("Only the first buffer may refer to the GLB BIN chunk"));
476 } else {
477 QString resolveError;
478 buffer.data = QSSGGltfResourceResolver::loadUri(buffer.uri, baseDir, &resolveError);
479 // Loading can legitimately produce nothing, for an empty file or an
480 // empty data URI payload, so the error string is what says whether
481 // it failed. The length check below rejects a short buffer anyway.
482 if (!resolveError.isEmpty())
483 return setError(QStringLiteral("Failed to load buffer %1: %2")
484 .arg(document->buffers.size()).arg(resolveError));
485 }
486 if (buffer.data.size() < buffer.byteLength)
487 return setError(QStringLiteral("Buffer %1 is %2 bytes, expected at least %3")
488 .arg(document->buffers.size()).arg(buffer.data.size()).arg(buffer.byteLength));
489 document->buffers.append(buffer);
490 }
491
492 // bufferViews
493 MeshoptDecodeState meshoptState;
494 const QJsonArray bufferViews = root.value(QLatin1String("bufferViews")).toArray();
495 for (const auto &value : bufferViews) {
496 const QJsonObject object = value.toObject();
497 BufferView view;
498 view.buffer = object.value(QLatin1String("buffer")).toInt(-1);
499 view.byteOffset = qint64(object.value(QLatin1String("byteOffset")).toDouble(0));
500 view.byteLength = qint64(object.value(QLatin1String("byteLength")).toDouble());
501 view.byteStride = object.value(QLatin1String("byteStride")).toInt(0);
502 view.target = object.value(QLatin1String("target")).toInt(0);
503 view.name = object.value(QLatin1String("name")).toString();
504
505 const QJsonValue meshoptValue = object.value(QLatin1String("extensions")).toObject()
506 .value(QLatin1String("EXT_meshopt_compression"));
507 if (meshoptValue.isObject()) {
508 QString decodeError;
509 if (!decodeMeshoptBufferView(meshoptValue.toObject(), *document, view, meshoptState, &decodeError)) {
510 return setError(QStringLiteral("Buffer view %1: EXT_meshopt_compression: %2")
511 .arg(document->bufferViews.size()).arg(decodeError));
512 }
513 document->bufferViews.append(view);
514 continue;
515 }
516
517 if (view.buffer < 0 || view.buffer >= document->buffers.size())
518 return setError(
519 QStringLiteral("Buffer view %1 references invalid buffer %2")
520 .arg(document->bufferViews.size()).arg(view.buffer));
521 if (document->buffers.at(view.buffer).meshoptFallback)
522 return setError(QStringLiteral("Buffer view %1 references unloaded fallback buffer %2")
523 .arg(document->bufferViews.size()).arg(view.buffer));
524 // The stride bound is from the specification; together with
525 // non-negative offsets and lengths it also keeps all later offset
526 // arithmetic far away from overflowing 64 bits.
527 if (view.byteOffset < 0 || view.byteLength < 0 || view.byteStride < 0 || view.byteStride > 252)
528 return setError(QStringLiteral("Buffer view %1 has an invalid byte offset, length, or stride")
529 .arg(document->bufferViews.size()));
530 // Compared without adding the two together: both are individually
531 // positive by the check above, but their sum can overflow to a negative
532 // value and slip under the buffer length.
533 const qint64 bufferLength = document->buffers.at(view.buffer).byteLength;
534 if (view.byteLength > bufferLength || view.byteOffset > bufferLength - view.byteLength)
535 return setError(
536 QStringLiteral("Buffer view %1 extends past the end of buffer %2")
537 .arg(document->bufferViews.size()).arg(view.buffer));
538 document->bufferViews.append(view);
539 }
540
541 // accessors
542 const QJsonArray accessors = root.value(QLatin1String("accessors")).toArray();
543 for (const auto &value : accessors) {
544 const QJsonObject object = value.toObject();
545 Accessor accessor;
546 accessor.bufferView = object.value(QLatin1String("bufferView")).toInt(-1);
547 accessor.byteOffset = qint64(object.value(QLatin1String("byteOffset")).toDouble(0));
548 const int componentType = object.value(QLatin1String("componentType")).toInt();
549 if (!isValidComponentType(componentType))
550 return setError(QStringLiteral("Accessor %1 has invalid component type %2")
551 .arg(document->accessors.size()).arg(componentType));
552 accessor.componentType = Accessor::ComponentType(componentType);
553 bool typeOk = false;
554 accessor.type = accessorTypeFromString(object.value(QLatin1String("type")).toString(), &typeOk);
555 if (!typeOk)
556 return setError(QStringLiteral("Accessor %1 has invalid type '%2'")
557 .arg(document->accessors.size())
558 .arg(object.value(QLatin1String("type")).toString()));
559 // The specification puts no upper bound on count, but the offset
560 // arithmetic below needs one. A count near the top of the qint64 range
561 // makes those products overflow, which is undefined behavior, and in
562 // practice wraps the range test at the end of this loop into an
563 // acceptance: a ten-line document then yields an accessor claiming
564 // more elements than could ever be allocated. One element occupies at
565 // least one byte, so no real asset comes anywhere near this cap, and
566 // capping in elements keeps every product here well inside 64 bits.
567 constexpr qint64 maxAccessorElements = qint64(256) * 1024 * 1024;
568 const double rawCount = object.value(QLatin1String("count")).toDouble();
569 if (!(rawCount >= 0.0 && rawCount <= double(maxAccessorElements)))
570 return setError(QStringLiteral("Accessor %1 has a negative or unreasonably large count")
571 .arg(document->accessors.size()));
572 accessor.count = qint64(rawCount);
573 if (accessor.byteOffset < 0)
574 return setError(QStringLiteral("Accessor %1 has a negative byte offset")
575 .arg(document->accessors.size()));
576 accessor.normalized = object.value(QLatin1String("normalized")).toBool(false);
577 accessor.name = object.value(QLatin1String("name")).toString();
578 const QJsonArray minValues = object.value(QLatin1String("min")).toArray();
579 for (const auto &m : minValues)
580 accessor.min.append(m.toDouble());
581 const QJsonArray maxValues = object.value(QLatin1String("max")).toArray();
582 for (const auto &m : maxValues)
583 accessor.max.append(m.toDouble());
584
585 const QJsonValue sparseValue = object.value(QLatin1String("sparse"));
586 if (sparseValue.isObject()) {
587 const QJsonObject sparseObject = sparseValue.toObject();
588 Accessor::Sparse sparse;
589 // Bounded the same way as accessor.count, and for the same reason:
590 // it is multiplied by a component size a few lines down
591 const double rawSparseCount = sparseObject.value(QLatin1String("count")).toDouble();
592 if (!(rawSparseCount >= 0.0 && rawSparseCount <= double(maxAccessorElements)))
593 return setError(QStringLiteral("Accessor %1 has a negative or unreasonably large sparse count")
594 .arg(document->accessors.size()));
595 sparse.count = qint64(rawSparseCount);
596 const QJsonObject indices = sparseObject.value(QLatin1String("indices")).toObject();
597 sparse.indicesBufferView = indices.value(QLatin1String("bufferView")).toInt(-1);
598 sparse.indicesByteOffset = qint64(indices.value(QLatin1String("byteOffset")).toDouble(0));
599 const int indicesComponentType = indices.value(QLatin1String("componentType")).toInt();
600 if (!isValidComponentType(indicesComponentType))
601 return setError(
602 QStringLiteral("Accessor %1 sparse indices have invalid component type")
603 .arg(document->accessors.size()));
604 sparse.indicesComponentType = Accessor::ComponentType(indicesComponentType);
605 const QJsonObject values = sparseObject.value(QLatin1String("values")).toObject();
606 sparse.valuesBufferView = values.value(QLatin1String("bufferView")).toInt(-1);
607 sparse.valuesByteOffset = qint64(values.value(QLatin1String("byteOffset")).toDouble(0));
608
609 if (sparse.indicesBufferView < 0 || sparse.indicesBufferView >= document->bufferViews.size()
610 || sparse.valuesBufferView < 0 || sparse.valuesBufferView >= document->bufferViews.size()) {
611 return setError(QStringLiteral("Accessor %1 sparse data references an invalid buffer view")
612 .arg(document->accessors.size()));
613 }
614 if (sparse.count > accessor.count
615 || sparse.indicesByteOffset < 0 || sparse.valuesByteOffset < 0) {
616 return setError(QStringLiteral("Accessor %1 has an invalid sparse count or byte offset")
617 .arg(document->accessors.size()));
618 }
619 // Offsets kept out of the sums, like the buffer view check above
620 const BufferView &indicesView = document->bufferViews.at(sparse.indicesBufferView);
621 const BufferView &valuesView = document->bufferViews.at(sparse.valuesBufferView);
622 const qint64 indicesSpan =
623 sparse.count * Accessor::componentByteSize(sparse.indicesComponentType);
624 const qint64 valuesSpan = sparse.count * accessor.elementByteSize();
625 if (indicesSpan > indicesView.byteLength
626 || sparse.indicesByteOffset > indicesView.byteLength - indicesSpan
627 || valuesSpan > valuesView.byteLength
628 || sparse.valuesByteOffset > valuesView.byteLength - valuesSpan) {
629 return setError(
630 QStringLiteral("Accessor %1 sparse data extends past the end of its buffer view")
631 .arg(document->accessors.size()));
632 }
633 accessor.sparse = sparse;
634 }
635
636 if (accessor.bufferView >= document->bufferViews.size())
637 return setError(
638 QStringLiteral("Accessor %1 references invalid buffer view %2")
639 .arg(document->accessors.size()).arg(accessor.bufferView));
640 if (accessor.bufferView >= 0) {
641 const BufferView &view = document->bufferViews.at(accessor.bufferView);
642 const qint64 elementSize = accessor.elementByteSize();
643 const qint64 stride = view.byteStride > 0 ? view.byteStride : elementSize;
644 // Offset kept out of the sum, like the buffer view check above
645 const qint64 span = (accessor.count - 1) * stride + elementSize;
646 if (accessor.count > 0
647 && (span > view.byteLength || accessor.byteOffset > view.byteLength - span))
648 return setError(QStringLiteral("Accessor %1 extends past the end of buffer view %2")
649 .arg(document->accessors.size())
650 .arg(accessor.bufferView));
651 } else {
652 // An accessor without a buffer view is read as all zeros, so no
653 // buffer bounds its size. The element cap above keeps this product
654 // from overflowing, but 256M elements of MAT4 would still be a
655 // 16 GB allocation, so bound the byte count as well.
656 constexpr qint64 maxSyntheticAccessorBytes = qint64(256) * 1024 * 1024;
657 if (accessor.count * accessor.elementByteSize() > maxSyntheticAccessorBytes)
658 return setError(QStringLiteral("Accessor %1 has no buffer view and an unreasonably large count")
659 .arg(document->accessors.size()));
660 }
661 document->accessors.append(accessor);
662 }
663
664 // images
665 const QJsonArray images = root.value(QLatin1String("images")).toArray();
666 for (const auto &value : images) {
667 const QJsonObject object = value.toObject();
668 Image image;
669 image.uri = object.value(QLatin1String("uri")).toString();
670 image.bufferView = object.value(QLatin1String("bufferView")).toInt(-1);
671 image.mimeType = object.value(QLatin1String("mimeType")).toString();
672 image.name = object.value(QLatin1String("name")).toString();
673 if (image.bufferView >= document->bufferViews.size())
674 return setError(QStringLiteral("Image %1 references invalid buffer view %2")
675 .arg(document->images.size()).arg(image.bufferView));
676 document->images.append(image);
677 }
678
679 // samplers
680 const QJsonArray samplers = root.value(QLatin1String("samplers")).toArray();
681 for (const auto &value : samplers) {
682 const QJsonObject object = value.toObject();
683 Sampler sampler;
684 sampler.magFilter = object.value(QLatin1String("magFilter")).toInt(0);
685 sampler.minFilter = object.value(QLatin1String("minFilter")).toInt(0);
686 sampler.wrapS = object.value(QLatin1String("wrapS")).toInt(Sampler::Repeat);
687 sampler.wrapT = object.value(QLatin1String("wrapT")).toInt(Sampler::Repeat);
688 sampler.name = object.value(QLatin1String("name")).toString();
689 document->samplers.append(sampler);
690 }
691
692 // textures
693 const QJsonArray textures = root.value(QLatin1String("textures")).toArray();
694 for (const auto &value : textures) {
695 const QJsonObject object = value.toObject();
696 Texture texture;
697 texture.sampler = object.value(QLatin1String("sampler")).toInt(-1);
698 texture.source = object.value(QLatin1String("source")).toInt(-1);
699 texture.name = object.value(QLatin1String("name")).toString();
700 texture.extensions = object.value(QLatin1String("extensions")).toObject();
701
702 const QJsonValue webpExt = texture.extensions.value(QLatin1String("EXT_texture_webp"));
703 if (webpExt.isObject())
704 texture.webpSource = webpExt.toObject().value(QLatin1String("source")).toInt(-1);
705
706 if (texture.sampler >= document->samplers.size())
707 return setError(QStringLiteral("Texture %1 references invalid sampler %2")
708 .arg(document->textures.size()).arg(texture.sampler));
709 if (texture.source >= document->images.size() || texture.webpSource >= document->images.size())
710 return setError(QStringLiteral("Texture %1 references invalid image %2")
711 .arg(document->textures.size())
712 .arg(qMax(texture.source, texture.webpSource)));
713 document->textures.append(texture);
714 }
715
716 // materials
717 //
718 // Every texture reference goes through parseTexture() rather than
719 // parseTextureInfo() directly, so that the index is range checked in one
720 // place and a newly supported extension cannot forget to do it. The
721 // textures array is already parsed at this point.
722 int badTextureIndex = -1;
723 const auto parseTexture = [&](const QJsonObject &object,
724 const char *scaleOrStrengthKey = nullptr) {
725 const TextureInfo info = parseTextureInfo(object, scaleOrStrengthKey);
726 if (info.index >= document->textures.size())
727 badTextureIndex = info.index;
728 return info;
729 };
730
731 const QJsonArray materials = root.value(QLatin1String("materials")).toArray();
732 for (const auto &value : materials) {
733 const QJsonObject object = value.toObject();
734 Material material;
735 material.name = object.value(QLatin1String("name")).toString();
736
737 const QJsonValue pbrValue = object.value(QLatin1String("pbrMetallicRoughness"));
738 if (pbrValue.isObject()) {
739 const QJsonObject pbr = pbrValue.toObject();
740 material.baseColorFactor =
741 toVector4D(pbr.value(QLatin1String("baseColorFactor")).toArray(), material.baseColorFactor);
742 material.baseColorTexture = parseTexture(pbr.value(QLatin1String("baseColorTexture")).toObject());
743 material.metallicFactor = float(pbr.value(QLatin1String("metallicFactor")).toDouble(1.0));
744 material.roughnessFactor = float(pbr.value(QLatin1String("roughnessFactor")).toDouble(1.0));
745 material.metallicRoughnessTexture =
746 parseTexture(pbr.value(QLatin1String("metallicRoughnessTexture")).toObject());
747 }
748
749 material.normalTexture = parseTexture(object.value(QLatin1String("normalTexture")).toObject(), "scale");
750 material.occlusionTexture =
751 parseTexture(object.value(QLatin1String("occlusionTexture")).toObject(), "strength");
752 material.emissiveTexture = parseTexture(object.value(QLatin1String("emissiveTexture")).toObject());
753 material.emissiveFactor =
754 toVector3D(object.value(QLatin1String("emissiveFactor")).toArray(), material.emissiveFactor);
755
756 const QString alphaMode = object.value(QLatin1String("alphaMode")).toString();
757 if (alphaMode == QLatin1String("MASK"))
758 material.alphaMode = Material::AlphaMode::Mask;
759 else if (alphaMode == QLatin1String("BLEND"))
760 material.alphaMode = Material::AlphaMode::Blend;
761 material.alphaCutoff = float(object.value(QLatin1String("alphaCutoff")).toDouble(0.5));
762 material.doubleSided = object.value(QLatin1String("doubleSided")).toBool(false);
763
764 material.extensions = object.value(QLatin1String("extensions")).toObject();
765 const QJsonObject &ext = material.extensions;
766
767 material.unlit = ext.contains(QLatin1String("KHR_materials_unlit"));
768
769 const QJsonValue sgValue = ext.value(QLatin1String("KHR_materials_pbrSpecularGlossiness"));
770 if (sgValue.isObject()) {
771 const QJsonObject sg = sgValue.toObject();
772 Material::SpecularGlossiness specularGlossiness;
773 specularGlossiness.diffuseFactor = toVector4D(sg.value(QLatin1String("diffuseFactor")).toArray(),
774 specularGlossiness.diffuseFactor);
775 specularGlossiness.diffuseTexture = parseTexture(sg.value(QLatin1String("diffuseTexture")).toObject());
776 specularGlossiness.specularFactor = toVector3D(sg.value(QLatin1String("specularFactor")).toArray(),
777 specularGlossiness.specularFactor);
778 specularGlossiness.glossinessFactor = float(sg.value(QLatin1String("glossinessFactor")).toDouble(1.0));
779 specularGlossiness.specularGlossinessTexture = parseTexture(
780 sg.value(QLatin1String("specularGlossinessTexture")).toObject());
781 material.specularGlossiness = specularGlossiness;
782 }
783
784 const QJsonValue ccValue = ext.value(QLatin1String("KHR_materials_clearcoat"));
785 if (ccValue.isObject()) {
786 const QJsonObject cc = ccValue.toObject();
787 Material::Clearcoat clearcoat;
788 clearcoat.clearcoatFactor = float(cc.value(QLatin1String("clearcoatFactor")).toDouble(0.0));
789 clearcoat.clearcoatTexture = parseTexture(cc.value(QLatin1String("clearcoatTexture")).toObject());
790 clearcoat.clearcoatRoughnessFactor =
791 float(cc.value(QLatin1String("clearcoatRoughnessFactor")).toDouble(0.0));
792 clearcoat.clearcoatRoughnessTexture =
793 parseTexture(cc.value(QLatin1String("clearcoatRoughnessTexture")).toObject());
794 clearcoat.clearcoatNormalTexture =
795 parseTexture(cc.value(QLatin1String("clearcoatNormalTexture")).toObject(), "scale");
796 material.clearcoat = clearcoat;
797 }
798
799 const QJsonValue trValue = ext.value(QLatin1String("KHR_materials_transmission"));
800 if (trValue.isObject()) {
801 const QJsonObject tr = trValue.toObject();
802 Material::Transmission transmission;
803 transmission.transmissionFactor = float(tr.value(QLatin1String("transmissionFactor")).toDouble(0.0));
804 transmission.transmissionTexture =
805 parseTexture(tr.value(QLatin1String("transmissionTexture")).toObject());
806 material.transmission = transmission;
807 }
808
809 const QJsonValue volValue = ext.value(QLatin1String("KHR_materials_volume"));
810 if (volValue.isObject()) {
811 const QJsonObject vol = volValue.toObject();
812 Material::Volume volume;
813 volume.thicknessFactor = float(vol.value(QLatin1String("thicknessFactor")).toDouble(0.0));
814 volume.thicknessTexture = parseTexture(vol.value(QLatin1String("thicknessTexture")).toObject());
815 volume.attenuationDistance = float(vol.value(QLatin1String("attenuationDistance")).toDouble(0.0));
816 volume.attenuationColor =
817 toVector3D(vol.value(QLatin1String("attenuationColor")).toArray(), volume.attenuationColor);
818 material.volume = volume;
819 }
820
821 const QJsonValue iorValue = ext.value(QLatin1String("KHR_materials_ior"));
822 if (iorValue.isObject())
823 material.ior = float(iorValue.toObject().value(QLatin1String("ior")).toDouble(1.5));
824
825 const QJsonValue esValue = ext.value(QLatin1String("KHR_materials_emissive_strength"));
826 if (esValue.isObject())
827 material.emissiveStrength =
828 float(esValue.toObject().value(QLatin1String("emissiveStrength")).toDouble(1.0));
829
830 const QJsonValue spValue = ext.value(QLatin1String("KHR_materials_specular"));
831 if (spValue.isObject()) {
832 const QJsonObject sp = spValue.toObject();
833 Material::Specular specular;
834 specular.specularFactor = float(sp.value(QLatin1String("specularFactor")).toDouble(1.0));
835 specular.specularTexture = parseTexture(sp.value(QLatin1String("specularTexture")).toObject());
836 specular.specularColorFactor = toVector3D(sp.value(QLatin1String("specularColorFactor")).toArray(),
837 specular.specularColorFactor);
838 specular.specularColorTexture =
839 parseTexture(sp.value(QLatin1String("specularColorTexture")).toObject());
840 material.specular = specular;
841 }
842
843 const QJsonValue shValue = ext.value(QLatin1String("KHR_materials_sheen"));
844 if (shValue.isObject()) {
845 const QJsonObject sh = shValue.toObject();
846 Material::Sheen sheen;
847 sheen.sheenColorFactor = toVector3D(sh.value(QLatin1String("sheenColorFactor")).toArray(), sheen.sheenColorFactor);
848 sheen.sheenColorTexture = parseTexture(sh.value(QLatin1String("sheenColorTexture")).toObject());
849 sheen.sheenRoughnessFactor = float(sh.value(QLatin1String("sheenRoughnessFactor")).toDouble(0.0));
850 sheen.sheenRoughnessTexture = parseTexture(sh.value(QLatin1String("sheenRoughnessTexture")).toObject());
851 material.sheen = sheen;
852 }
853
854 const QJsonValue anValue = ext.value(QLatin1String("KHR_materials_anisotropy"));
855 if (anValue.isObject()) {
856 const QJsonObject an = anValue.toObject();
857 Material::Anisotropy anisotropy;
858 anisotropy.anisotropyStrength = float(an.value(QLatin1String("anisotropyStrength")).toDouble(0.0));
859 anisotropy.anisotropyRotation = float(an.value(QLatin1String("anisotropyRotation")).toDouble(0.0));
860 anisotropy.anisotropyTexture = parseTexture(an.value(QLatin1String("anisotropyTexture")).toObject());
861 material.anisotropy = anisotropy;
862 }
863
864 const QJsonValue irValue = ext.value(QLatin1String("KHR_materials_iridescence"));
865 if (irValue.isObject()) {
866 const QJsonObject ir = irValue.toObject();
867 Material::Iridescence iridescence;
868 iridescence.iridescenceFactor = float(ir.value(QLatin1String("iridescenceFactor")).toDouble(0.0));
869 iridescence.iridescenceTexture = parseTexture(ir.value(QLatin1String("iridescenceTexture")).toObject());
870 iridescence.iridescenceIor = float(ir.value(QLatin1String("iridescenceIor")).toDouble(1.3));
871 iridescence.iridescenceThicknessMinimum = float(ir.value(QLatin1String("iridescenceThicknessMinimum")).toDouble(100.0));
872 iridescence.iridescenceThicknessMaximum = float(ir.value(QLatin1String("iridescenceThicknessMaximum")).toDouble(400.0));
873 iridescence.iridescenceThicknessTexture = parseTexture(
874 ir.value(QLatin1String("iridescenceThicknessTexture")).toObject());
875 material.iridescence = iridescence;
876 }
877
878 const QJsonValue dispValue = ext.value(QLatin1String("KHR_materials_dispersion"));
879 if (dispValue.isObject())
880 material.dispersion = float(dispValue.toObject().value(QLatin1String("dispersion")).toDouble(0.0));
881
882 document->materials.append(material);
883 }
884
885 if (badTextureIndex >= 0)
886 return setError(QStringLiteral("Material references invalid texture %1").arg(badTextureIndex));
887
888 // meshes
889 const QJsonArray meshes = root.value(QLatin1String("meshes")).toArray();
890 for (const auto &value : meshes) {
891 const QJsonObject object = value.toObject();
892 Mesh mesh;
893 mesh.name = object.value(QLatin1String("name")).toString();
894 const QJsonArray weights = object.value(QLatin1String("weights")).toArray();
895 for (const auto &w : weights)
896 mesh.weights.append(float(w.toDouble()));
897
898 const QJsonArray primitives = object.value(QLatin1String("primitives")).toArray();
899 for (const auto &primitiveValue : primitives) {
900 const QJsonObject primitiveObject = primitiveValue.toObject();
901 MeshPrimitive primitive;
902 const QJsonObject attributes = primitiveObject.value(QLatin1String("attributes")).toObject();
903 for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it)
904 primitive.attributes.insert(it.key().toUtf8(), it.value().toInt(-1));
905 primitive.indices = primitiveObject.value(QLatin1String("indices")).toInt(-1);
906 primitive.material = primitiveObject.value(QLatin1String("material")).toInt(-1);
907 primitive.mode = primitiveObject.value(QLatin1String("mode")).toInt(MeshPrimitive::Triangles);
908 const QJsonArray targets = primitiveObject.value(QLatin1String("targets")).toArray();
909 for (const auto &targetValue : targets) {
910 const QJsonObject targetObject = targetValue.toObject();
911 QHash<QByteArray, int> target;
912 for (auto it = targetObject.constBegin(); it != targetObject.constEnd(); ++it)
913 target.insert(it.key().toUtf8(), it.value().toInt(-1));
914 primitive.targets.append(target);
915 }
916 primitive.extensions = primitiveObject.value(QLatin1String("extensions")).toObject();
917
918 const QJsonValue variantsExt = primitive.extensions.value(QLatin1String("KHR_materials_variants"));
919 if (variantsExt.isObject()) {
920 const QJsonArray mappings = variantsExt.toObject().value(QLatin1String("mappings")).toArray();
921 for (const auto &mappingValue : mappings) {
922 const QJsonObject mappingObject = mappingValue.toObject();
923 MeshPrimitive::VariantMapping mapping;
924 mapping.material = mappingObject.value(QLatin1String("material")).toInt(-1);
925 if (mapping.material < 0 || mapping.material >= document->materials.size()) {
926 return setError(
927 QStringLiteral("Mesh %1 primitive variant mapping references invalid material %2")
928 .arg(document->meshes.size())
929 .arg(mapping.material));
930 }
931 const QJsonArray mappingVariants = mappingObject.value(QLatin1String("variants")).toArray();
932 for (const auto &variantValue : mappingVariants) {
933 const int variant = variantValue.toInt(-1);
934 if (variant < 0 || variant >= document->materialVariants.size()) {
935 return setError(
936 QStringLiteral("Mesh %1 primitive variant mapping references invalid variant %2")
937 .arg(document->meshes.size())
938 .arg(variant));
939 }
940 mapping.variants.append(variant);
941 }
942 primitive.variantMappings.append(mapping);
943 }
944 }
945
946 // Structural validation of accessor references. As everywhere in
947 // this parser, any negative index means "unset": the glTF defaults
948 // are -1, the readers return nothing for a negative index, and so
949 // only the upper bound needs checking here.
950 const auto checkAccessor = [&](int accessor, const char *what) {
951 if (accessor >= document->accessors.size())
952 return setError(QStringLiteral("Mesh %1 primitive references invalid %2 accessor %3")
953 .arg(document->meshes.size()).arg(QLatin1String(what)).arg(accessor));
954 return true;
955 };
956 for (auto it = primitive.attributes.constBegin(); it != primitive.attributes.constEnd(); ++it) {
957 if (!checkAccessor(it.value(), it.key().constData()))
958 return false;
959 }
960 if (primitive.indices >= 0 && !checkAccessor(primitive.indices, "index"))
961 return false;
962 if (primitive.material >= document->materials.size())
963 return setError(QStringLiteral("Mesh %1 primitive references invalid material %2")
964 .arg(document->meshes.size())
965 .arg(primitive.material));
966
967 mesh.primitives.append(primitive);
968 }
969 if (mesh.primitives.isEmpty())
970 qCWarning(lcQuick3DGltf) << "Mesh" << document->meshes.size() << "has no primitives";
971 document->meshes.append(mesh);
972 }
973
974 // cameras
975 const QJsonArray cameras = root.value(QLatin1String("cameras")).toArray();
976 for (const auto &value : cameras) {
977 const QJsonObject object = value.toObject();
978 Camera camera;
979 camera.name = object.value(QLatin1String("name")).toString();
980 const QString type = object.value(QLatin1String("type")).toString();
981 if (type == QLatin1String("orthographic")) {
982 camera.type = Camera::Type::Orthographic;
983 const QJsonObject ortho = object.value(QLatin1String("orthographic")).toObject();
984 camera.xmag = float(ortho.value(QLatin1String("xmag")).toDouble());
985 camera.ymag = float(ortho.value(QLatin1String("ymag")).toDouble());
986 camera.znear = float(ortho.value(QLatin1String("znear")).toDouble());
987 camera.zfar = float(ortho.value(QLatin1String("zfar")).toDouble());
988 } else {
989 camera.type = Camera::Type::Perspective;
990 const QJsonObject perspective = object.value(QLatin1String("perspective")).toObject();
991 camera.aspectRatio = float(perspective.value(QLatin1String("aspectRatio")).toDouble(0.0));
992 camera.yfov = float(perspective.value(QLatin1String("yfov")).toDouble());
993 camera.znear = float(perspective.value(QLatin1String("znear")).toDouble());
994 camera.zfar = float(perspective.value(QLatin1String("zfar")).toDouble(0.0));
995 }
996 document->cameras.append(camera);
997 }
998
999 // KHR_lights_punctual (document level)
1000 {
1001 const QJsonValue lightsExt = document->rootExtensions.value(QLatin1String("KHR_lights_punctual"));
1002 if (lightsExt.isObject()) {
1003 const QJsonArray lights = lightsExt.toObject().value(QLatin1String("lights")).toArray();
1004 for (const auto &value : lights) {
1005 const QJsonObject object = value.toObject();
1006 Light light;
1007 light.name = object.value(QLatin1String("name")).toString();
1008 const QString type = object.value(QLatin1String("type")).toString();
1009 if (type == QLatin1String("point"))
1010 light.type = Light::Type::Point;
1011 else if (type == QLatin1String("spot"))
1012 light.type = Light::Type::Spot;
1013 else
1014 light.type = Light::Type::Directional;
1015 light.color = toVector3D(object.value(QLatin1String("color")).toArray(), light.color);
1016 light.intensity = float(object.value(QLatin1String("intensity")).toDouble(1.0));
1017 light.range = float(object.value(QLatin1String("range")).toDouble(0.0));
1018 if (light.type == Light::Type::Spot) {
1019 const QJsonObject spot = object.value(QLatin1String("spot")).toObject();
1020 light.innerConeAngle = float(spot.value(QLatin1String("innerConeAngle")).toDouble(0.0));
1021 light.outerConeAngle = float(spot.value(QLatin1String("outerConeAngle")).toDouble(M_PI_4));
1022 }
1023 document->lights.append(light);
1024 }
1025 }
1026 }
1027
1028 // nodes
1029 const QJsonArray nodes = root.value(QLatin1String("nodes")).toArray();
1030 for (const auto &value : nodes) {
1031 const QJsonObject object = value.toObject();
1032 Node node;
1033 node.name = object.value(QLatin1String("name")).toString();
1034 const QJsonArray children = object.value(QLatin1String("children")).toArray();
1035 for (const auto &child : children)
1036 node.children.append(child.toInt(-1));
1037 node.mesh = object.value(QLatin1String("mesh")).toInt(-1);
1038 node.skin = object.value(QLatin1String("skin")).toInt(-1);
1039 node.camera = object.value(QLatin1String("camera")).toInt(-1);
1040
1041 const QJsonValue matrixValue = object.value(QLatin1String("matrix"));
1042 if (matrixValue.isArray()) {
1043 const QJsonArray m = matrixValue.toArray();
1044 if (m.size() == 16) {
1045 float values[16];
1046 for (int i = 0; i < 16; ++i)
1047 values[i] = float(m.at(i).toDouble());
1048 // glTF matrices are column-major; QMatrix4x4(float*) is row-major
1049 node.matrix = QMatrix4x4(values).transposed();
1050 node.hasMatrix = true;
1051 }
1052 }
1053 node.translation = toVector3D(object.value(QLatin1String("translation")).toArray(), node.translation);
1054 const QJsonArray rotation = object.value(QLatin1String("rotation")).toArray();
1055 if (rotation.size() == 4) {
1056 // glTF quaternions are (x, y, z, w)
1057 node.rotation = QQuaternion(float(rotation.at(3).toDouble()), float(rotation.at(0).toDouble()),
1058 float(rotation.at(1).toDouble()), float(rotation.at(2).toDouble()));
1059 }
1060 node.scale = toVector3D(object.value(QLatin1String("scale")).toArray(), node.scale);
1061 const QJsonArray weights = object.value(QLatin1String("weights")).toArray();
1062 for (const auto &w : weights)
1063 node.weights.append(float(w.toDouble()));
1064 node.extensions = object.value(QLatin1String("extensions")).toObject();
1065
1066 const QJsonValue lightExt = node.extensions.value(QLatin1String("KHR_lights_punctual"));
1067 if (lightExt.isObject())
1068 node.light = lightExt.toObject().value(QLatin1String("light")).toInt(-1);
1069
1070 const QJsonValue visibilityExt = node.extensions.value(QLatin1String("KHR_node_visibility"));
1071 if (visibilityExt.isObject())
1072 node.visible = visibilityExt.toObject().value(QLatin1String("visible")).toBool(true);
1073
1074 const QJsonValue instancingExt = node.extensions.value(QLatin1String("EXT_mesh_gpu_instancing"));
1075 if (instancingExt.isObject()) {
1076 const QJsonObject instanceAttributes =
1077 instancingExt.toObject().value(QLatin1String("attributes")).toObject();
1078 for (auto it = instanceAttributes.constBegin(); it != instanceAttributes.constEnd(); ++it) {
1079 const int accessor = it.value().toInt(-1);
1080 if (accessor < 0 || accessor >= document->accessors.size()) {
1081 return setError(QStringLiteral("Node %1 instance attribute %2 references invalid accessor %3")
1082 .arg(document->nodes.size()).arg(it.key()).arg(accessor));
1083 }
1084 if (it.key() == QLatin1String("TRANSLATION"))
1085 node.instanceTranslation = accessor;
1086 else if (it.key() == QLatin1String("ROTATION"))
1087 node.instanceRotation = accessor;
1088 else if (it.key() == QLatin1String("SCALE"))
1089 node.instanceScale = accessor;
1090 else
1091 qCWarning(lcQuick3DGltf) << "Ignoring unsupported instance attribute" << it.key();
1092 }
1093 node.hasInstancing = node.instanceTranslation >= 0 || node.instanceRotation >= 0
1094 || node.instanceScale >= 0;
1095 }
1096
1097 if (node.mesh >= document->meshes.size())
1098 return setError(QStringLiteral("Node %1 references invalid mesh %2")
1099 .arg(document->nodes.size()).arg(node.mesh));
1100 if (node.camera >= document->cameras.size())
1101 return setError(QStringLiteral("Node %1 references invalid camera %2")
1102 .arg(document->nodes.size()).arg(node.camera));
1103 if (node.light >= document->lights.size())
1104 return setError(QStringLiteral("Node %1 references invalid light %2")
1105 .arg(document->nodes.size()).arg(node.light));
1106 document->nodes.append(node);
1107 }
1108
1109 // Validate node graph references (children and skins can be forward
1110 // references). The specification requires the hierarchy to be a forest,
1111 // and consumers that recurse over the graph rely on that to be safe from
1112 // cycles and exponential blowup, so the shape is repaired here rather than
1113 // left for them to cope with.
1114 const int nodeCount = document->nodes.size();
1115 QVarLengthArray<int, 64> parentCount(nodeCount);
1116 std::fill(parentCount.begin(), parentCount.end(), 0);
1117 for (int i = 0; i < nodeCount; ++i) {
1118 Node &node = document->nodes[i];
1119 for (qsizetype c = 0; c < node.children.size(); ++c) {
1120 const int child = node.children.at(c);
1121 if (child < 0 || child >= nodeCount)
1122 return setError(QStringLiteral("Node %1 references invalid child node %2").arg(i).arg(child));
1123 if (++parentCount[child] > 1) {
1124 // A second parent cannot be represented at all: a node
1125 // occupies exactly one place in a scene graph. Keep the first
1126 // edge and drop this one, so the rest of the asset still
1127 // loads.
1128 qCWarning(lcQuick3DGltf) << "Node" << child
1129 << "has more than one parent; ignoring the edge from node" << i;
1130 --parentCount[child];
1131 node.children.removeAt(c--);
1132 }
1133 }
1134 }
1135 // With single parents the only remaining hazards are parentless cycles
1136 // and excessive depth; both are caught by walking up each node's parent
1137 // chain, memoizing the depths so every edge is visited only once.
1138 {
1139 constexpr int maxNodeDepth = 4096;
1140 QVarLengthArray<int, 64> parent(nodeCount);
1141 std::fill(parent.begin(), parent.end(), -1);
1142 for (int i = 0; i < nodeCount; ++i) {
1143 for (int child : document->nodes.at(i).children)
1144 parent[child] = i;
1145 }
1146 QVarLengthArray<int, 64> depth(nodeCount);
1147 std::fill(depth.begin(), depth.end(), -1);
1148 QVarLengthArray<int, 64> chain;
1149 for (int i = 0; i < nodeCount; ++i) {
1150 chain.clear();
1151 int n = i;
1152 while (n >= 0 && depth[n] < 0 && chain.size() <= nodeCount) {
1153 chain.append(n);
1154 n = parent[n];
1155 }
1156 if (chain.size() > nodeCount)
1157 return setError(QStringLiteral("Node hierarchy contains a cycle"));
1158 int d = n >= 0 ? depth[n] : 0;
1159 for (auto it = chain.rbegin(); it != chain.rend(); ++it)
1160 depth[*it] = ++d;
1161 if (d > maxNodeDepth)
1162 return setError(QStringLiteral("Node hierarchy is deeper than %1").arg(maxNodeDepth));
1163 }
1164 }
1165
1166 // skins
1167 const QJsonArray skins = root.value(QLatin1String("skins")).toArray();
1168 for (const auto &value : skins) {
1169 const QJsonObject object = value.toObject();
1170 Skin skin;
1171 skin.name = object.value(QLatin1String("name")).toString();
1172 skin.inverseBindMatrices = object.value(QLatin1String("inverseBindMatrices")).toInt(-1);
1173 skin.skeleton = object.value(QLatin1String("skeleton")).toInt(-1);
1174 const QJsonArray joints = object.value(QLatin1String("joints")).toArray();
1175 for (const auto &joint : joints)
1176 skin.joints.append(joint.toInt(-1));
1177
1178 if (skin.inverseBindMatrices >= document->accessors.size())
1179 return setError(QStringLiteral("Skin %1 references invalid accessor %2")
1180 .arg(document->skins.size()).arg(skin.inverseBindMatrices));
1181 for (int joint : std::as_const(skin.joints)) {
1182 if (joint < 0 || joint >= nodeCount)
1183 return setError(QStringLiteral("Skin %1 references invalid joint node %2")
1184 .arg(document->skins.size()).arg(joint));
1185 }
1186 document->skins.append(skin);
1187 }
1188 for (int i = 0; i < nodeCount; ++i) {
1189 if (document->nodes.at(i).skin >= document->skins.size())
1190 return setError(QStringLiteral("Node %1 references invalid skin %2")
1191 .arg(i).arg(document->nodes.at(i).skin));
1192 }
1193
1194 // animations
1195 const QJsonArray animations = root.value(QLatin1String("animations")).toArray();
1196 for (const auto &value : animations) {
1197 const QJsonObject object = value.toObject();
1198 Animation animation;
1199 animation.name = object.value(QLatin1String("name")).toString();
1200
1201 const QJsonArray animationSamplers = object.value(QLatin1String("samplers")).toArray();
1202 for (const auto &samplerValue : animationSamplers) {
1203 const QJsonObject samplerObject = samplerValue.toObject();
1204 AnimationSampler sampler;
1205 sampler.input = samplerObject.value(QLatin1String("input")).toInt(-1);
1206 sampler.output = samplerObject.value(QLatin1String("output")).toInt(-1);
1207 const QString interpolation = samplerObject.value(QLatin1String("interpolation")).toString();
1208 if (interpolation == QLatin1String("STEP"))
1209 sampler.interpolation = AnimationSampler::Interpolation::Step;
1210 else if (interpolation == QLatin1String("CUBICSPLINE"))
1211 sampler.interpolation = AnimationSampler::Interpolation::CubicSpline;
1212 if (sampler.input < 0 || sampler.input >= document->accessors.size()
1213 || sampler.output < 0 || sampler.output >= document->accessors.size()) {
1214 return setError(
1215 QStringLiteral("Animation %1 sampler references an invalid accessor")
1216 .arg(document->animations.size()));
1217 }
1218 animation.samplers.append(sampler);
1219 }
1220
1221 const QJsonArray channels = object.value(QLatin1String("channels")).toArray();
1222 for (const auto &channelValue : channels) {
1223 const QJsonObject channelObject = channelValue.toObject();
1224 AnimationChannel channel;
1225 channel.sampler = channelObject.value(QLatin1String("sampler")).toInt(-1);
1226 const QJsonObject target = channelObject.value(QLatin1String("target")).toObject();
1227 channel.targetNode = target.value(QLatin1String("node")).toInt(-1);
1228 const QString path = target.value(QLatin1String("path")).toString();
1229 if (path == QLatin1String("rotation"))
1230 channel.path = AnimationChannel::Path::Rotation;
1231 else if (path == QLatin1String("scale"))
1232 channel.path = AnimationChannel::Path::Scale;
1233 else if (path == QLatin1String("weights"))
1234 channel.path = AnimationChannel::Path::Weights;
1235 else if (path != QLatin1String("translation")) {
1236 qCWarning(lcQuick3DGltf) << "Ignoring animation channel with unsupported path" << path;
1237 continue;
1238 }
1239 if (channel.sampler < 0 || channel.sampler >= animation.samplers.size())
1240 return setError(QStringLiteral("Animation %1 channel references invalid sampler %2")
1241 .arg(document->animations.size())
1242 .arg(channel.sampler));
1243 if (channel.targetNode >= nodeCount)
1244 return setError(QStringLiteral("Animation %1 channel references invalid node %2")
1245 .arg(document->animations.size())
1246 .arg(channel.targetNode));
1247 animation.channels.append(channel);
1248 }
1249
1250 document->animations.append(animation);
1251 }
1252
1253 // scenes
1254 const QJsonArray scenes = root.value(QLatin1String("scenes")).toArray();
1255 for (const auto &value : scenes) {
1256 const QJsonObject object = value.toObject();
1257 Scene scene;
1258 scene.name = object.value(QLatin1String("name")).toString();
1259 QSet<int> seenRoots;
1260 const QJsonArray sceneNodes = object.value(QLatin1String("nodes")).toArray();
1261 for (const auto &nodeIndex : sceneNodes) {
1262 const int index = nodeIndex.toInt(-1);
1263 if (index < 0 || index >= nodeCount)
1264 return setError(QStringLiteral("Scene %1 references invalid node %2")
1265 .arg(document->scenes.size()).arg(index));
1266 // Scene nodes must be distinct root nodes; a repeated node, or one
1267 // that is also somebody's child, would have its subtree emitted
1268 // more than once. Skip the offending entry rather than failing, so
1269 // that the scene still contains everything it should, once.
1270 if (parentCount[index] != 0 || seenRoots.contains(index)) {
1271 qCWarning(lcQuick3DGltf) << "Scene" << document->scenes.size() << "references node" << index
1272 << "which is not a unique root node; ignoring it";
1273 continue;
1274 }
1275 seenRoots.insert(index);
1276 scene.nodes.append(index);
1277 }
1278 document->scenes.append(scene);
1279 }
1280 document->scene = root.value(QLatin1String("scene")).toInt(-1);
1281 if (document->scene >= document->scenes.size())
1282 return setError(QStringLiteral("Document references invalid default scene %1").arg(document->scene));
1283
1284 return true;
1285}
1286
1287QT_END_NAMESPACE
Combined button and popup list for selecting options.