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 const quint32 chunkLength = qFromLittleEndian<quint32>(data.constData() + offset);
361 const quint32 chunkType = qFromLittleEndian<quint32>(data.constData() + offset + 4);
362 offset += 8;
363 if (offset + qint64(chunkLength) > qint64(length))
364 return setError(QStringLiteral("Truncated GLB chunk at offset %1").arg(offset - 8));
365
366 if (chunkType == GLB_CHUNK_JSON && json.isEmpty())
367 json = data.mid(offset, chunkLength);
368 else if (chunkType == GLB_CHUNK_BIN && binChunk.isEmpty())
369 binChunk = data.mid(offset, chunkLength);
370 else
371 qCWarning(lcQuick3DGltf) << "Skipping unknown GLB chunk type" << Qt::hex << chunkType;
372
373 // Chunks are 4-byte aligned
374 offset += chunkLength;
375 if (offset % 4)
376 offset += 4 - (offset % 4);
377 }
378 if (json.isEmpty())
379 return setError(QStringLiteral("GLB container has no JSON chunk"));
380 }
381
382 QJsonParseError jsonError;
383 const QJsonDocument jsonDocument = QJsonDocument::fromJson(json, &jsonError);
384 if (jsonDocument.isNull())
385 return setError(QStringLiteral("Invalid glTF JSON: %1").arg(jsonError.errorString()));
386 if (!jsonDocument.isObject())
387 return setError(QStringLiteral("Invalid glTF JSON: root is not an object"));
388
389 const QJsonObject root = jsonDocument.object();
390
391 // asset (required)
392 {
393 const QJsonValue assetValue = root.value(QLatin1String("asset"));
394 if (!assetValue.isObject())
395 return setError(QStringLiteral("Not a glTF document: no asset object"));
396 const QJsonObject asset = assetValue.toObject();
397 document->asset.version = asset.value(QLatin1String("version")).toString();
398 document->asset.minVersion = asset.value(QLatin1String("minVersion")).toString();
399 document->asset.generator = asset.value(QLatin1String("generator")).toString();
400 document->asset.copyright = asset.value(QLatin1String("copyright")).toString();
401
402 const int major = document->asset.version.section(QLatin1Char('.'), 0, 0).toInt();
403 if (major != 2)
404 return setError(QStringLiteral("Unsupported glTF version '%1'").arg(document->asset.version));
405
406 // The specification requires a client to fail when minVersion asks for
407 // more than it implements. Only 2.0 exists so far, so this is about not
408 // silently loading a future asset that says it needs more.
409 if (!document->asset.minVersion.isEmpty()) {
410 const int minMajor = document->asset.minVersion.section(QLatin1Char('.'), 0, 0).toInt();
411 const int minMinor = document->asset.minVersion.section(QLatin1Char('.'), 1, 1).toInt();
412 if (minMajor != 2 || minMinor > 0)
413 return setError(QStringLiteral("Asset requires glTF version '%1' or higher")
414 .arg(document->asset.minVersion));
415 }
416 }
417
418 // extensionsUsed / extensionsRequired
419 for (const auto &value : root.value(QLatin1String("extensionsUsed")).toArray())
420 document->extensionsUsed.append(value.toString());
421 for (const auto &value : root.value(QLatin1String("extensionsRequired")).toArray())
422 document->extensionsRequired.append(value.toString());
423 document->rootExtensions = root.value(QLatin1String("extensions")).toObject();
424
425 // KHR_materials_variants (document level): the variant names
426 {
427 const QJsonValue variantsExt = document->rootExtensions.value(QLatin1String("KHR_materials_variants"));
428 if (variantsExt.isObject()) {
429 for (const auto &value : variantsExt.toObject().value(QLatin1String("variants")).toArray())
430 document->materialVariants.append(value.toObject().value(QLatin1String("name")).toString());
431 }
432 }
433
434 const QStringList supported = supportedExtensions();
435 for (const QString &required : std::as_const(document->extensionsRequired)) {
436 if (!supported.contains(required))
437 return setError(QStringLiteral("Asset requires unsupported glTF extension '%1'").arg(required));
438 }
439 for (const QString &used : std::as_const(document->extensionsUsed)) {
440 if (!supported.contains(used))
441 qCWarning(lcQuick3DGltf) << "Ignoring unsupported glTF extension" << used;
442 }
443
444 // buffers
445 for (const auto &value : root.value(QLatin1String("buffers")).toArray()) {
446 const QJsonObject object = value.toObject();
447 Buffer buffer;
448 buffer.uri = object.value(QLatin1String("uri")).toString();
449 buffer.byteLength = qint64(object.value(QLatin1String("byteLength")).toDouble());
450 if (buffer.byteLength < 0)
451 return setError(QStringLiteral("Buffer %1 has negative byte length").arg(document->buffers.size()));
452 buffer.meshoptFallback = object.value(QLatin1String("extensions")).toObject()
453 .value(QLatin1String("EXT_meshopt_compression")).toObject()
454 .value(QLatin1String("fallback")).toBool(false);
455 if (buffer.meshoptFallback) {
456 document->buffers.append(buffer);
457 continue;
458 }
459 if (buffer.uri.isEmpty()) {
460 // GLB BIN chunk; only valid for the first buffer
461 if (document->buffers.isEmpty() && !binChunk.isEmpty())
462 buffer.data = binChunk;
463 else if (binChunk.isEmpty())
464 return setError(
465 QStringLiteral("Buffer %1 has no URI and there is no GLB BIN chunk")
466 .arg(document->buffers.size()));
467 else
468 return setError(QStringLiteral("Only the first buffer may refer to the GLB BIN chunk"));
469 } else {
470 QString resolveError;
471 buffer.data = QSSGGltfResourceResolver::loadUri(buffer.uri, baseDir, &resolveError);
472 // Loading can legitimately produce nothing, for an empty file or an
473 // empty data URI payload, so the error string is what says whether
474 // it failed. The length check below rejects a short buffer anyway.
475 if (!resolveError.isEmpty())
476 return setError(QStringLiteral("Failed to load buffer %1: %2")
477 .arg(document->buffers.size()).arg(resolveError));
478 }
479 if (buffer.data.size() < buffer.byteLength)
480 return setError(QStringLiteral("Buffer %1 is %2 bytes, expected at least %3")
481 .arg(document->buffers.size()).arg(buffer.data.size()).arg(buffer.byteLength));
482 document->buffers.append(buffer);
483 }
484
485 // bufferViews
486 MeshoptDecodeState meshoptState;
487 for (const auto &value : root.value(QLatin1String("bufferViews")).toArray()) {
488 const QJsonObject object = value.toObject();
489 BufferView view;
490 view.buffer = object.value(QLatin1String("buffer")).toInt(-1);
491 view.byteOffset = qint64(object.value(QLatin1String("byteOffset")).toDouble(0));
492 view.byteLength = qint64(object.value(QLatin1String("byteLength")).toDouble());
493 view.byteStride = object.value(QLatin1String("byteStride")).toInt(0);
494 view.target = object.value(QLatin1String("target")).toInt(0);
495 view.name = object.value(QLatin1String("name")).toString();
496
497 const QJsonValue meshoptValue = object.value(QLatin1String("extensions")).toObject()
498 .value(QLatin1String("EXT_meshopt_compression"));
499 if (meshoptValue.isObject()) {
500 QString decodeError;
501 if (!decodeMeshoptBufferView(meshoptValue.toObject(), *document, view, meshoptState, &decodeError)) {
502 return setError(QStringLiteral("Buffer view %1: EXT_meshopt_compression: %2")
503 .arg(document->bufferViews.size()).arg(decodeError));
504 }
505 document->bufferViews.append(view);
506 continue;
507 }
508
509 if (view.buffer < 0 || view.buffer >= document->buffers.size())
510 return setError(
511 QStringLiteral("Buffer view %1 references invalid buffer %2")
512 .arg(document->bufferViews.size()).arg(view.buffer));
513 if (document->buffers.at(view.buffer).meshoptFallback)
514 return setError(QStringLiteral("Buffer view %1 references unloaded fallback buffer %2")
515 .arg(document->bufferViews.size()).arg(view.buffer));
516 // The stride bound is from the specification; together with
517 // non-negative offsets and lengths it also keeps all later offset
518 // arithmetic far away from overflowing 64 bits.
519 if (view.byteOffset < 0 || view.byteLength < 0 || view.byteStride < 0 || view.byteStride > 252)
520 return setError(QStringLiteral("Buffer view %1 has an invalid byte offset, length, or stride")
521 .arg(document->bufferViews.size()));
522 // Compared without adding the two together: both are individually
523 // positive by the check above, but their sum can overflow to a negative
524 // value and slip under the buffer length.
525 const qint64 bufferLength = document->buffers.at(view.buffer).byteLength;
526 if (view.byteLength > bufferLength || view.byteOffset > bufferLength - view.byteLength)
527 return setError(
528 QStringLiteral("Buffer view %1 extends past the end of buffer %2")
529 .arg(document->bufferViews.size()).arg(view.buffer));
530 document->bufferViews.append(view);
531 }
532
533 // accessors
534 for (const auto &value : root.value(QLatin1String("accessors")).toArray()) {
535 const QJsonObject object = value.toObject();
536 Accessor accessor;
537 accessor.bufferView = object.value(QLatin1String("bufferView")).toInt(-1);
538 accessor.byteOffset = qint64(object.value(QLatin1String("byteOffset")).toDouble(0));
539 const int componentType = object.value(QLatin1String("componentType")).toInt();
540 if (!isValidComponentType(componentType))
541 return setError(QStringLiteral("Accessor %1 has invalid component type %2")
542 .arg(document->accessors.size()).arg(componentType));
543 accessor.componentType = Accessor::ComponentType(componentType);
544 bool typeOk = false;
545 accessor.type = accessorTypeFromString(object.value(QLatin1String("type")).toString(), &typeOk);
546 if (!typeOk)
547 return setError(QStringLiteral("Accessor %1 has invalid type '%2'")
548 .arg(document->accessors.size())
549 .arg(object.value(QLatin1String("type")).toString()));
550 // The specification puts no upper bound on count, but the offset
551 // arithmetic below needs one. A count near the top of the qint64 range
552 // makes those products overflow, which is undefined behavior, and in
553 // practice wraps the range test at the end of this loop into an
554 // acceptance: a ten-line document then yields an accessor claiming
555 // more elements than could ever be allocated. One element occupies at
556 // least one byte, so no real asset comes anywhere near this cap, and
557 // capping in elements keeps every product here well inside 64 bits.
558 constexpr qint64 maxAccessorElements = qint64(256) * 1024 * 1024;
559 const double rawCount = object.value(QLatin1String("count")).toDouble();
560 if (!(rawCount >= 0.0 && rawCount <= double(maxAccessorElements)))
561 return setError(QStringLiteral("Accessor %1 has a negative or unreasonably large count")
562 .arg(document->accessors.size()));
563 accessor.count = qint64(rawCount);
564 if (accessor.byteOffset < 0)
565 return setError(QStringLiteral("Accessor %1 has a negative byte offset")
566 .arg(document->accessors.size()));
567 accessor.normalized = object.value(QLatin1String("normalized")).toBool(false);
568 accessor.name = object.value(QLatin1String("name")).toString();
569 for (const auto &m : object.value(QLatin1String("min")).toArray())
570 accessor.min.append(m.toDouble());
571 for (const auto &m : object.value(QLatin1String("max")).toArray())
572 accessor.max.append(m.toDouble());
573
574 const QJsonValue sparseValue = object.value(QLatin1String("sparse"));
575 if (sparseValue.isObject()) {
576 const QJsonObject sparseObject = sparseValue.toObject();
577 Accessor::Sparse sparse;
578 // Bounded the same way as accessor.count, and for the same reason:
579 // it is multiplied by a component size a few lines down
580 const double rawSparseCount = sparseObject.value(QLatin1String("count")).toDouble();
581 if (!(rawSparseCount >= 0.0 && rawSparseCount <= double(maxAccessorElements)))
582 return setError(QStringLiteral("Accessor %1 has a negative or unreasonably large sparse count")
583 .arg(document->accessors.size()));
584 sparse.count = qint64(rawSparseCount);
585 const QJsonObject indices = sparseObject.value(QLatin1String("indices")).toObject();
586 sparse.indicesBufferView = indices.value(QLatin1String("bufferView")).toInt(-1);
587 sparse.indicesByteOffset = qint64(indices.value(QLatin1String("byteOffset")).toDouble(0));
588 const int indicesComponentType = indices.value(QLatin1String("componentType")).toInt();
589 if (!isValidComponentType(indicesComponentType))
590 return setError(
591 QStringLiteral("Accessor %1 sparse indices have invalid component type")
592 .arg(document->accessors.size()));
593 sparse.indicesComponentType = Accessor::ComponentType(indicesComponentType);
594 const QJsonObject values = sparseObject.value(QLatin1String("values")).toObject();
595 sparse.valuesBufferView = values.value(QLatin1String("bufferView")).toInt(-1);
596 sparse.valuesByteOffset = qint64(values.value(QLatin1String("byteOffset")).toDouble(0));
597
598 if (sparse.indicesBufferView < 0 || sparse.indicesBufferView >= document->bufferViews.size()
599 || sparse.valuesBufferView < 0 || sparse.valuesBufferView >= document->bufferViews.size()) {
600 return setError(QStringLiteral("Accessor %1 sparse data references an invalid buffer view")
601 .arg(document->accessors.size()));
602 }
603 if (sparse.count > accessor.count
604 || sparse.indicesByteOffset < 0 || sparse.valuesByteOffset < 0) {
605 return setError(QStringLiteral("Accessor %1 has an invalid sparse count or byte offset")
606 .arg(document->accessors.size()));
607 }
608 // Offsets kept out of the sums, like the buffer view check above
609 const BufferView &indicesView = document->bufferViews.at(sparse.indicesBufferView);
610 const BufferView &valuesView = document->bufferViews.at(sparse.valuesBufferView);
611 const qint64 indicesSpan =
612 sparse.count * Accessor::componentByteSize(sparse.indicesComponentType);
613 const qint64 valuesSpan = sparse.count * accessor.elementByteSize();
614 if (indicesSpan > indicesView.byteLength
615 || sparse.indicesByteOffset > indicesView.byteLength - indicesSpan
616 || valuesSpan > valuesView.byteLength
617 || sparse.valuesByteOffset > valuesView.byteLength - valuesSpan) {
618 return setError(
619 QStringLiteral("Accessor %1 sparse data extends past the end of its buffer view")
620 .arg(document->accessors.size()));
621 }
622 accessor.sparse = sparse;
623 }
624
625 if (accessor.bufferView >= document->bufferViews.size())
626 return setError(
627 QStringLiteral("Accessor %1 references invalid buffer view %2")
628 .arg(document->accessors.size()).arg(accessor.bufferView));
629 if (accessor.bufferView >= 0) {
630 const BufferView &view = document->bufferViews.at(accessor.bufferView);
631 const qint64 elementSize = accessor.elementByteSize();
632 const qint64 stride = view.byteStride > 0 ? view.byteStride : elementSize;
633 // Offset kept out of the sum, like the buffer view check above
634 const qint64 span = (accessor.count - 1) * stride + elementSize;
635 if (accessor.count > 0
636 && (span > view.byteLength || accessor.byteOffset > view.byteLength - span))
637 return setError(QStringLiteral("Accessor %1 extends past the end of buffer view %2")
638 .arg(document->accessors.size())
639 .arg(accessor.bufferView));
640 } else {
641 // An accessor without a buffer view is read as all zeros, so no
642 // buffer bounds its size. The element cap above keeps this product
643 // from overflowing, but 256M elements of MAT4 would still be a
644 // 16 GB allocation, so bound the byte count as well.
645 constexpr qint64 maxSyntheticAccessorBytes = qint64(256) * 1024 * 1024;
646 if (accessor.count * accessor.elementByteSize() > maxSyntheticAccessorBytes)
647 return setError(QStringLiteral("Accessor %1 has no buffer view and an unreasonably large count")
648 .arg(document->accessors.size()));
649 }
650 document->accessors.append(accessor);
651 }
652
653 // images
654 for (const auto &value : root.value(QLatin1String("images")).toArray()) {
655 const QJsonObject object = value.toObject();
656 Image image;
657 image.uri = object.value(QLatin1String("uri")).toString();
658 image.bufferView = object.value(QLatin1String("bufferView")).toInt(-1);
659 image.mimeType = object.value(QLatin1String("mimeType")).toString();
660 image.name = object.value(QLatin1String("name")).toString();
661 if (image.bufferView >= document->bufferViews.size())
662 return setError(QStringLiteral("Image %1 references invalid buffer view %2")
663 .arg(document->images.size()).arg(image.bufferView));
664 document->images.append(image);
665 }
666
667 // samplers
668 for (const auto &value : root.value(QLatin1String("samplers")).toArray()) {
669 const QJsonObject object = value.toObject();
670 Sampler sampler;
671 sampler.magFilter = object.value(QLatin1String("magFilter")).toInt(0);
672 sampler.minFilter = object.value(QLatin1String("minFilter")).toInt(0);
673 sampler.wrapS = object.value(QLatin1String("wrapS")).toInt(Sampler::Repeat);
674 sampler.wrapT = object.value(QLatin1String("wrapT")).toInt(Sampler::Repeat);
675 sampler.name = object.value(QLatin1String("name")).toString();
676 document->samplers.append(sampler);
677 }
678
679 // textures
680 for (const auto &value : root.value(QLatin1String("textures")).toArray()) {
681 const QJsonObject object = value.toObject();
682 Texture texture;
683 texture.sampler = object.value(QLatin1String("sampler")).toInt(-1);
684 texture.source = object.value(QLatin1String("source")).toInt(-1);
685 texture.name = object.value(QLatin1String("name")).toString();
686 texture.extensions = object.value(QLatin1String("extensions")).toObject();
687
688 const QJsonValue webpExt = texture.extensions.value(QLatin1String("EXT_texture_webp"));
689 if (webpExt.isObject())
690 texture.webpSource = webpExt.toObject().value(QLatin1String("source")).toInt(-1);
691
692 if (texture.sampler >= document->samplers.size())
693 return setError(QStringLiteral("Texture %1 references invalid sampler %2")
694 .arg(document->textures.size()).arg(texture.sampler));
695 if (texture.source >= document->images.size() || texture.webpSource >= document->images.size())
696 return setError(QStringLiteral("Texture %1 references invalid image %2")
697 .arg(document->textures.size())
698 .arg(qMax(texture.source, texture.webpSource)));
699 document->textures.append(texture);
700 }
701
702 // materials
703 //
704 // Every texture reference goes through parseTexture() rather than
705 // parseTextureInfo() directly, so that the index is range checked in one
706 // place and a newly supported extension cannot forget to do it. The
707 // textures array is already parsed at this point.
708 int badTextureIndex = -1;
709 const auto parseTexture = [&](const QJsonObject &object,
710 const char *scaleOrStrengthKey = nullptr) {
711 const TextureInfo info = parseTextureInfo(object, scaleOrStrengthKey);
712 if (info.index >= document->textures.size())
713 badTextureIndex = info.index;
714 return info;
715 };
716
717 for (const auto &value : root.value(QLatin1String("materials")).toArray()) {
718 const QJsonObject object = value.toObject();
719 Material material;
720 material.name = object.value(QLatin1String("name")).toString();
721
722 const QJsonValue pbrValue = object.value(QLatin1String("pbrMetallicRoughness"));
723 if (pbrValue.isObject()) {
724 const QJsonObject pbr = pbrValue.toObject();
725 material.baseColorFactor =
726 toVector4D(pbr.value(QLatin1String("baseColorFactor")).toArray(), material.baseColorFactor);
727 material.baseColorTexture = parseTexture(pbr.value(QLatin1String("baseColorTexture")).toObject());
728 material.metallicFactor = float(pbr.value(QLatin1String("metallicFactor")).toDouble(1.0));
729 material.roughnessFactor = float(pbr.value(QLatin1String("roughnessFactor")).toDouble(1.0));
730 material.metallicRoughnessTexture =
731 parseTexture(pbr.value(QLatin1String("metallicRoughnessTexture")).toObject());
732 }
733
734 material.normalTexture = parseTexture(object.value(QLatin1String("normalTexture")).toObject(), "scale");
735 material.occlusionTexture =
736 parseTexture(object.value(QLatin1String("occlusionTexture")).toObject(), "strength");
737 material.emissiveTexture = parseTexture(object.value(QLatin1String("emissiveTexture")).toObject());
738 material.emissiveFactor =
739 toVector3D(object.value(QLatin1String("emissiveFactor")).toArray(), material.emissiveFactor);
740
741 const QString alphaMode = object.value(QLatin1String("alphaMode")).toString();
742 if (alphaMode == QLatin1String("MASK"))
743 material.alphaMode = Material::AlphaMode::Mask;
744 else if (alphaMode == QLatin1String("BLEND"))
745 material.alphaMode = Material::AlphaMode::Blend;
746 material.alphaCutoff = float(object.value(QLatin1String("alphaCutoff")).toDouble(0.5));
747 material.doubleSided = object.value(QLatin1String("doubleSided")).toBool(false);
748
749 material.extensions = object.value(QLatin1String("extensions")).toObject();
750 const QJsonObject &ext = material.extensions;
751
752 material.unlit = ext.contains(QLatin1String("KHR_materials_unlit"));
753
754 const QJsonValue sgValue = ext.value(QLatin1String("KHR_materials_pbrSpecularGlossiness"));
755 if (sgValue.isObject()) {
756 const QJsonObject sg = sgValue.toObject();
757 Material::SpecularGlossiness specularGlossiness;
758 specularGlossiness.diffuseFactor = toVector4D(sg.value(QLatin1String("diffuseFactor")).toArray(),
759 specularGlossiness.diffuseFactor);
760 specularGlossiness.diffuseTexture = parseTexture(sg.value(QLatin1String("diffuseTexture")).toObject());
761 specularGlossiness.specularFactor = toVector3D(sg.value(QLatin1String("specularFactor")).toArray(),
762 specularGlossiness.specularFactor);
763 specularGlossiness.glossinessFactor = float(sg.value(QLatin1String("glossinessFactor")).toDouble(1.0));
764 specularGlossiness.specularGlossinessTexture = parseTexture(
765 sg.value(QLatin1String("specularGlossinessTexture")).toObject());
766 material.specularGlossiness = specularGlossiness;
767 }
768
769 const QJsonValue ccValue = ext.value(QLatin1String("KHR_materials_clearcoat"));
770 if (ccValue.isObject()) {
771 const QJsonObject cc = ccValue.toObject();
772 Material::Clearcoat clearcoat;
773 clearcoat.clearcoatFactor = float(cc.value(QLatin1String("clearcoatFactor")).toDouble(0.0));
774 clearcoat.clearcoatTexture = parseTexture(cc.value(QLatin1String("clearcoatTexture")).toObject());
775 clearcoat.clearcoatRoughnessFactor =
776 float(cc.value(QLatin1String("clearcoatRoughnessFactor")).toDouble(0.0));
777 clearcoat.clearcoatRoughnessTexture =
778 parseTexture(cc.value(QLatin1String("clearcoatRoughnessTexture")).toObject());
779 clearcoat.clearcoatNormalTexture =
780 parseTexture(cc.value(QLatin1String("clearcoatNormalTexture")).toObject(), "scale");
781 material.clearcoat = clearcoat;
782 }
783
784 const QJsonValue trValue = ext.value(QLatin1String("KHR_materials_transmission"));
785 if (trValue.isObject()) {
786 const QJsonObject tr = trValue.toObject();
787 Material::Transmission transmission;
788 transmission.transmissionFactor = float(tr.value(QLatin1String("transmissionFactor")).toDouble(0.0));
789 transmission.transmissionTexture =
790 parseTexture(tr.value(QLatin1String("transmissionTexture")).toObject());
791 material.transmission = transmission;
792 }
793
794 const QJsonValue volValue = ext.value(QLatin1String("KHR_materials_volume"));
795 if (volValue.isObject()) {
796 const QJsonObject vol = volValue.toObject();
797 Material::Volume volume;
798 volume.thicknessFactor = float(vol.value(QLatin1String("thicknessFactor")).toDouble(0.0));
799 volume.thicknessTexture = parseTexture(vol.value(QLatin1String("thicknessTexture")).toObject());
800 volume.attenuationDistance = float(vol.value(QLatin1String("attenuationDistance")).toDouble(0.0));
801 volume.attenuationColor =
802 toVector3D(vol.value(QLatin1String("attenuationColor")).toArray(), volume.attenuationColor);
803 material.volume = volume;
804 }
805
806 const QJsonValue iorValue = ext.value(QLatin1String("KHR_materials_ior"));
807 if (iorValue.isObject())
808 material.ior = float(iorValue.toObject().value(QLatin1String("ior")).toDouble(1.5));
809
810 const QJsonValue esValue = ext.value(QLatin1String("KHR_materials_emissive_strength"));
811 if (esValue.isObject())
812 material.emissiveStrength =
813 float(esValue.toObject().value(QLatin1String("emissiveStrength")).toDouble(1.0));
814
815 const QJsonValue spValue = ext.value(QLatin1String("KHR_materials_specular"));
816 if (spValue.isObject()) {
817 const QJsonObject sp = spValue.toObject();
818 Material::Specular specular;
819 specular.specularFactor = float(sp.value(QLatin1String("specularFactor")).toDouble(1.0));
820 specular.specularTexture = parseTexture(sp.value(QLatin1String("specularTexture")).toObject());
821 specular.specularColorFactor = toVector3D(sp.value(QLatin1String("specularColorFactor")).toArray(),
822 specular.specularColorFactor);
823 specular.specularColorTexture =
824 parseTexture(sp.value(QLatin1String("specularColorTexture")).toObject());
825 material.specular = specular;
826 }
827
828 const QJsonValue shValue = ext.value(QLatin1String("KHR_materials_sheen"));
829 if (shValue.isObject()) {
830 const QJsonObject sh = shValue.toObject();
831 Material::Sheen sheen;
832 sheen.sheenColorFactor = toVector3D(sh.value(QLatin1String("sheenColorFactor")).toArray(), sheen.sheenColorFactor);
833 sheen.sheenColorTexture = parseTexture(sh.value(QLatin1String("sheenColorTexture")).toObject());
834 sheen.sheenRoughnessFactor = float(sh.value(QLatin1String("sheenRoughnessFactor")).toDouble(0.0));
835 sheen.sheenRoughnessTexture = parseTexture(sh.value(QLatin1String("sheenRoughnessTexture")).toObject());
836 material.sheen = sheen;
837 }
838
839 const QJsonValue anValue = ext.value(QLatin1String("KHR_materials_anisotropy"));
840 if (anValue.isObject()) {
841 const QJsonObject an = anValue.toObject();
842 Material::Anisotropy anisotropy;
843 anisotropy.anisotropyStrength = float(an.value(QLatin1String("anisotropyStrength")).toDouble(0.0));
844 anisotropy.anisotropyRotation = float(an.value(QLatin1String("anisotropyRotation")).toDouble(0.0));
845 anisotropy.anisotropyTexture = parseTexture(an.value(QLatin1String("anisotropyTexture")).toObject());
846 material.anisotropy = anisotropy;
847 }
848
849 const QJsonValue irValue = ext.value(QLatin1String("KHR_materials_iridescence"));
850 if (irValue.isObject()) {
851 const QJsonObject ir = irValue.toObject();
852 Material::Iridescence iridescence;
853 iridescence.iridescenceFactor = float(ir.value(QLatin1String("iridescenceFactor")).toDouble(0.0));
854 iridescence.iridescenceTexture = parseTexture(ir.value(QLatin1String("iridescenceTexture")).toObject());
855 iridescence.iridescenceIor = float(ir.value(QLatin1String("iridescenceIor")).toDouble(1.3));
856 iridescence.iridescenceThicknessMinimum = float(ir.value(QLatin1String("iridescenceThicknessMinimum")).toDouble(100.0));
857 iridescence.iridescenceThicknessMaximum = float(ir.value(QLatin1String("iridescenceThicknessMaximum")).toDouble(400.0));
858 iridescence.iridescenceThicknessTexture = parseTexture(
859 ir.value(QLatin1String("iridescenceThicknessTexture")).toObject());
860 material.iridescence = iridescence;
861 }
862
863 const QJsonValue dispValue = ext.value(QLatin1String("KHR_materials_dispersion"));
864 if (dispValue.isObject())
865 material.dispersion = float(dispValue.toObject().value(QLatin1String("dispersion")).toDouble(0.0));
866
867 document->materials.append(material);
868 }
869
870 if (badTextureIndex >= 0)
871 return setError(QStringLiteral("Material references invalid texture %1").arg(badTextureIndex));
872
873 // meshes
874 for (const auto &value : root.value(QLatin1String("meshes")).toArray()) {
875 const QJsonObject object = value.toObject();
876 Mesh mesh;
877 mesh.name = object.value(QLatin1String("name")).toString();
878 for (const auto &w : object.value(QLatin1String("weights")).toArray())
879 mesh.weights.append(float(w.toDouble()));
880
881 for (const auto &primitiveValue : object.value(QLatin1String("primitives")).toArray()) {
882 const QJsonObject primitiveObject = primitiveValue.toObject();
883 MeshPrimitive primitive;
884 const QJsonObject attributes = primitiveObject.value(QLatin1String("attributes")).toObject();
885 for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it)
886 primitive.attributes.insert(it.key().toUtf8(), it.value().toInt(-1));
887 primitive.indices = primitiveObject.value(QLatin1String("indices")).toInt(-1);
888 primitive.material = primitiveObject.value(QLatin1String("material")).toInt(-1);
889 primitive.mode = primitiveObject.value(QLatin1String("mode")).toInt(MeshPrimitive::Triangles);
890 for (const auto &targetValue : primitiveObject.value(QLatin1String("targets")).toArray()) {
891 const QJsonObject targetObject = targetValue.toObject();
892 QHash<QByteArray, int> target;
893 for (auto it = targetObject.constBegin(); it != targetObject.constEnd(); ++it)
894 target.insert(it.key().toUtf8(), it.value().toInt(-1));
895 primitive.targets.append(target);
896 }
897 primitive.extensions = primitiveObject.value(QLatin1String("extensions")).toObject();
898
899 const QJsonValue variantsExt = primitive.extensions.value(QLatin1String("KHR_materials_variants"));
900 if (variantsExt.isObject()) {
901 for (const auto &mappingValue : variantsExt.toObject().value(QLatin1String("mappings")).toArray()) {
902 const QJsonObject mappingObject = mappingValue.toObject();
903 MeshPrimitive::VariantMapping mapping;
904 mapping.material = mappingObject.value(QLatin1String("material")).toInt(-1);
905 if (mapping.material < 0 || mapping.material >= document->materials.size()) {
906 return setError(
907 QStringLiteral("Mesh %1 primitive variant mapping references invalid material %2")
908 .arg(document->meshes.size())
909 .arg(mapping.material));
910 }
911 for (const auto &variantValue : mappingObject.value(QLatin1String("variants")).toArray()) {
912 const int variant = variantValue.toInt(-1);
913 if (variant < 0 || variant >= document->materialVariants.size()) {
914 return setError(
915 QStringLiteral("Mesh %1 primitive variant mapping references invalid variant %2")
916 .arg(document->meshes.size())
917 .arg(variant));
918 }
919 mapping.variants.append(variant);
920 }
921 primitive.variantMappings.append(mapping);
922 }
923 }
924
925 // Structural validation of accessor references. As everywhere in
926 // this parser, any negative index means "unset": the glTF defaults
927 // are -1, the readers return nothing for a negative index, and so
928 // only the upper bound needs checking here.
929 const auto checkAccessor = [&](int accessor, const char *what) {
930 if (accessor >= document->accessors.size())
931 return setError(QStringLiteral("Mesh %1 primitive references invalid %2 accessor %3")
932 .arg(document->meshes.size()).arg(QLatin1String(what)).arg(accessor));
933 return true;
934 };
935 for (auto it = primitive.attributes.constBegin(); it != primitive.attributes.constEnd(); ++it) {
936 if (!checkAccessor(it.value(), it.key().constData()))
937 return false;
938 }
939 if (primitive.indices >= 0 && !checkAccessor(primitive.indices, "index"))
940 return false;
941 if (primitive.material >= document->materials.size())
942 return setError(QStringLiteral("Mesh %1 primitive references invalid material %2")
943 .arg(document->meshes.size())
944 .arg(primitive.material));
945
946 mesh.primitives.append(primitive);
947 }
948 if (mesh.primitives.isEmpty())
949 qCWarning(lcQuick3DGltf) << "Mesh" << document->meshes.size() << "has no primitives";
950 document->meshes.append(mesh);
951 }
952
953 // cameras
954 for (const auto &value : root.value(QLatin1String("cameras")).toArray()) {
955 const QJsonObject object = value.toObject();
956 Camera camera;
957 camera.name = object.value(QLatin1String("name")).toString();
958 const QString type = object.value(QLatin1String("type")).toString();
959 if (type == QLatin1String("orthographic")) {
960 camera.type = Camera::Type::Orthographic;
961 const QJsonObject ortho = object.value(QLatin1String("orthographic")).toObject();
962 camera.xmag = float(ortho.value(QLatin1String("xmag")).toDouble());
963 camera.ymag = float(ortho.value(QLatin1String("ymag")).toDouble());
964 camera.znear = float(ortho.value(QLatin1String("znear")).toDouble());
965 camera.zfar = float(ortho.value(QLatin1String("zfar")).toDouble());
966 } else {
967 camera.type = Camera::Type::Perspective;
968 const QJsonObject perspective = object.value(QLatin1String("perspective")).toObject();
969 camera.aspectRatio = float(perspective.value(QLatin1String("aspectRatio")).toDouble(0.0));
970 camera.yfov = float(perspective.value(QLatin1String("yfov")).toDouble());
971 camera.znear = float(perspective.value(QLatin1String("znear")).toDouble());
972 camera.zfar = float(perspective.value(QLatin1String("zfar")).toDouble(0.0));
973 }
974 document->cameras.append(camera);
975 }
976
977 // KHR_lights_punctual (document level)
978 {
979 const QJsonValue lightsExt = document->rootExtensions.value(QLatin1String("KHR_lights_punctual"));
980 if (lightsExt.isObject()) {
981 for (const auto &value : lightsExt.toObject().value(QLatin1String("lights")).toArray()) {
982 const QJsonObject object = value.toObject();
983 Light light;
984 light.name = object.value(QLatin1String("name")).toString();
985 const QString type = object.value(QLatin1String("type")).toString();
986 if (type == QLatin1String("point"))
987 light.type = Light::Type::Point;
988 else if (type == QLatin1String("spot"))
989 light.type = Light::Type::Spot;
990 else
991 light.type = Light::Type::Directional;
992 light.color = toVector3D(object.value(QLatin1String("color")).toArray(), light.color);
993 light.intensity = float(object.value(QLatin1String("intensity")).toDouble(1.0));
994 light.range = float(object.value(QLatin1String("range")).toDouble(0.0));
995 if (light.type == Light::Type::Spot) {
996 const QJsonObject spot = object.value(QLatin1String("spot")).toObject();
997 light.innerConeAngle = float(spot.value(QLatin1String("innerConeAngle")).toDouble(0.0));
998 light.outerConeAngle = float(spot.value(QLatin1String("outerConeAngle")).toDouble(M_PI_4));
999 }
1000 document->lights.append(light);
1001 }
1002 }
1003 }
1004
1005 // nodes
1006 for (const auto &value : root.value(QLatin1String("nodes")).toArray()) {
1007 const QJsonObject object = value.toObject();
1008 Node node;
1009 node.name = object.value(QLatin1String("name")).toString();
1010 for (const auto &child : object.value(QLatin1String("children")).toArray())
1011 node.children.append(child.toInt(-1));
1012 node.mesh = object.value(QLatin1String("mesh")).toInt(-1);
1013 node.skin = object.value(QLatin1String("skin")).toInt(-1);
1014 node.camera = object.value(QLatin1String("camera")).toInt(-1);
1015
1016 const QJsonValue matrixValue = object.value(QLatin1String("matrix"));
1017 if (matrixValue.isArray()) {
1018 const QJsonArray m = matrixValue.toArray();
1019 if (m.size() == 16) {
1020 float values[16];
1021 for (int i = 0; i < 16; ++i)
1022 values[i] = float(m.at(i).toDouble());
1023 // glTF matrices are column-major; QMatrix4x4(float*) is row-major
1024 node.matrix = QMatrix4x4(values).transposed();
1025 node.hasMatrix = true;
1026 }
1027 }
1028 node.translation = toVector3D(object.value(QLatin1String("translation")).toArray(), node.translation);
1029 const QJsonArray rotation = object.value(QLatin1String("rotation")).toArray();
1030 if (rotation.size() == 4) {
1031 // glTF quaternions are (x, y, z, w)
1032 node.rotation = QQuaternion(float(rotation.at(3).toDouble()), float(rotation.at(0).toDouble()),
1033 float(rotation.at(1).toDouble()), float(rotation.at(2).toDouble()));
1034 }
1035 node.scale = toVector3D(object.value(QLatin1String("scale")).toArray(), node.scale);
1036 for (const auto &w : object.value(QLatin1String("weights")).toArray())
1037 node.weights.append(float(w.toDouble()));
1038 node.extensions = object.value(QLatin1String("extensions")).toObject();
1039
1040 const QJsonValue lightExt = node.extensions.value(QLatin1String("KHR_lights_punctual"));
1041 if (lightExt.isObject())
1042 node.light = lightExt.toObject().value(QLatin1String("light")).toInt(-1);
1043
1044 const QJsonValue visibilityExt = node.extensions.value(QLatin1String("KHR_node_visibility"));
1045 if (visibilityExt.isObject())
1046 node.visible = visibilityExt.toObject().value(QLatin1String("visible")).toBool(true);
1047
1048 const QJsonValue instancingExt = node.extensions.value(QLatin1String("EXT_mesh_gpu_instancing"));
1049 if (instancingExt.isObject()) {
1050 const QJsonObject instanceAttributes =
1051 instancingExt.toObject().value(QLatin1String("attributes")).toObject();
1052 for (auto it = instanceAttributes.constBegin(); it != instanceAttributes.constEnd(); ++it) {
1053 const int accessor = it.value().toInt(-1);
1054 if (accessor < 0 || accessor >= document->accessors.size()) {
1055 return setError(QStringLiteral("Node %1 instance attribute %2 references invalid accessor %3")
1056 .arg(document->nodes.size()).arg(it.key()).arg(accessor));
1057 }
1058 if (it.key() == QLatin1String("TRANSLATION"))
1059 node.instanceTranslation = accessor;
1060 else if (it.key() == QLatin1String("ROTATION"))
1061 node.instanceRotation = accessor;
1062 else if (it.key() == QLatin1String("SCALE"))
1063 node.instanceScale = accessor;
1064 else
1065 qCWarning(lcQuick3DGltf) << "Ignoring unsupported instance attribute" << it.key();
1066 }
1067 node.hasInstancing = node.instanceTranslation >= 0 || node.instanceRotation >= 0
1068 || node.instanceScale >= 0;
1069 }
1070
1071 if (node.mesh >= document->meshes.size())
1072 return setError(QStringLiteral("Node %1 references invalid mesh %2")
1073 .arg(document->nodes.size()).arg(node.mesh));
1074 if (node.camera >= document->cameras.size())
1075 return setError(QStringLiteral("Node %1 references invalid camera %2")
1076 .arg(document->nodes.size()).arg(node.camera));
1077 if (node.light >= document->lights.size())
1078 return setError(QStringLiteral("Node %1 references invalid light %2")
1079 .arg(document->nodes.size()).arg(node.light));
1080 document->nodes.append(node);
1081 }
1082
1083 // Validate node graph references (children and skins can be forward
1084 // references). The specification requires the hierarchy to be a forest,
1085 // and consumers that recurse over the graph rely on that to be safe from
1086 // cycles and exponential blowup, so the shape is repaired here rather than
1087 // left for them to cope with.
1088 const int nodeCount = document->nodes.size();
1089 QVarLengthArray<int, 64> parentCount(nodeCount);
1090 std::fill(parentCount.begin(), parentCount.end(), 0);
1091 for (int i = 0; i < nodeCount; ++i) {
1092 Node &node = document->nodes[i];
1093 for (qsizetype c = 0; c < node.children.size(); ++c) {
1094 const int child = node.children.at(c);
1095 if (child < 0 || child >= nodeCount)
1096 return setError(QStringLiteral("Node %1 references invalid child node %2").arg(i).arg(child));
1097 if (++parentCount[child] > 1) {
1098 // A second parent cannot be represented at all: a node
1099 // occupies exactly one place in a scene graph. Keep the first
1100 // edge and drop this one, so the rest of the asset still
1101 // loads.
1102 qCWarning(lcQuick3DGltf) << "Node" << child
1103 << "has more than one parent; ignoring the edge from node" << i;
1104 --parentCount[child];
1105 node.children.removeAt(c--);
1106 }
1107 }
1108 }
1109 // With single parents the only remaining hazards are parentless cycles
1110 // and excessive depth; both are caught by walking up each node's parent
1111 // chain, memoizing the depths so every edge is visited only once.
1112 {
1113 constexpr int maxNodeDepth = 4096;
1114 QVarLengthArray<int, 64> parent(nodeCount);
1115 std::fill(parent.begin(), parent.end(), -1);
1116 for (int i = 0; i < nodeCount; ++i) {
1117 for (int child : document->nodes.at(i).children)
1118 parent[child] = i;
1119 }
1120 QVarLengthArray<int, 64> depth(nodeCount);
1121 std::fill(depth.begin(), depth.end(), -1);
1122 QVarLengthArray<int, 64> chain;
1123 for (int i = 0; i < nodeCount; ++i) {
1124 chain.clear();
1125 int n = i;
1126 while (n >= 0 && depth[n] < 0 && chain.size() <= nodeCount) {
1127 chain.append(n);
1128 n = parent[n];
1129 }
1130 if (chain.size() > nodeCount)
1131 return setError(QStringLiteral("Node hierarchy contains a cycle"));
1132 int d = n >= 0 ? depth[n] : 0;
1133 for (auto it = chain.rbegin(); it != chain.rend(); ++it)
1134 depth[*it] = ++d;
1135 if (d > maxNodeDepth)
1136 return setError(QStringLiteral("Node hierarchy is deeper than %1").arg(maxNodeDepth));
1137 }
1138 }
1139
1140 // skins
1141 for (const auto &value : root.value(QLatin1String("skins")).toArray()) {
1142 const QJsonObject object = value.toObject();
1143 Skin skin;
1144 skin.name = object.value(QLatin1String("name")).toString();
1145 skin.inverseBindMatrices = object.value(QLatin1String("inverseBindMatrices")).toInt(-1);
1146 skin.skeleton = object.value(QLatin1String("skeleton")).toInt(-1);
1147 for (const auto &joint : object.value(QLatin1String("joints")).toArray())
1148 skin.joints.append(joint.toInt(-1));
1149
1150 if (skin.inverseBindMatrices >= document->accessors.size())
1151 return setError(QStringLiteral("Skin %1 references invalid accessor %2")
1152 .arg(document->skins.size()).arg(skin.inverseBindMatrices));
1153 for (int joint : std::as_const(skin.joints)) {
1154 if (joint < 0 || joint >= nodeCount)
1155 return setError(QStringLiteral("Skin %1 references invalid joint node %2")
1156 .arg(document->skins.size()).arg(joint));
1157 }
1158 document->skins.append(skin);
1159 }
1160 for (int i = 0; i < nodeCount; ++i) {
1161 if (document->nodes.at(i).skin >= document->skins.size())
1162 return setError(QStringLiteral("Node %1 references invalid skin %2")
1163 .arg(i).arg(document->nodes.at(i).skin));
1164 }
1165
1166 // animations
1167 for (const auto &value : root.value(QLatin1String("animations")).toArray()) {
1168 const QJsonObject object = value.toObject();
1169 Animation animation;
1170 animation.name = object.value(QLatin1String("name")).toString();
1171
1172 for (const auto &samplerValue : object.value(QLatin1String("samplers")).toArray()) {
1173 const QJsonObject samplerObject = samplerValue.toObject();
1174 AnimationSampler sampler;
1175 sampler.input = samplerObject.value(QLatin1String("input")).toInt(-1);
1176 sampler.output = samplerObject.value(QLatin1String("output")).toInt(-1);
1177 const QString interpolation = samplerObject.value(QLatin1String("interpolation")).toString();
1178 if (interpolation == QLatin1String("STEP"))
1179 sampler.interpolation = AnimationSampler::Interpolation::Step;
1180 else if (interpolation == QLatin1String("CUBICSPLINE"))
1181 sampler.interpolation = AnimationSampler::Interpolation::CubicSpline;
1182 if (sampler.input < 0 || sampler.input >= document->accessors.size()
1183 || sampler.output < 0 || sampler.output >= document->accessors.size()) {
1184 return setError(
1185 QStringLiteral("Animation %1 sampler references an invalid accessor")
1186 .arg(document->animations.size()));
1187 }
1188 animation.samplers.append(sampler);
1189 }
1190
1191 for (const auto &channelValue : object.value(QLatin1String("channels")).toArray()) {
1192 const QJsonObject channelObject = channelValue.toObject();
1193 AnimationChannel channel;
1194 channel.sampler = channelObject.value(QLatin1String("sampler")).toInt(-1);
1195 const QJsonObject target = channelObject.value(QLatin1String("target")).toObject();
1196 channel.targetNode = target.value(QLatin1String("node")).toInt(-1);
1197 const QString path = target.value(QLatin1String("path")).toString();
1198 if (path == QLatin1String("rotation"))
1199 channel.path = AnimationChannel::Path::Rotation;
1200 else if (path == QLatin1String("scale"))
1201 channel.path = AnimationChannel::Path::Scale;
1202 else if (path == QLatin1String("weights"))
1203 channel.path = AnimationChannel::Path::Weights;
1204 else if (path != QLatin1String("translation")) {
1205 qCWarning(lcQuick3DGltf) << "Ignoring animation channel with unsupported path" << path;
1206 continue;
1207 }
1208 if (channel.sampler < 0 || channel.sampler >= animation.samplers.size())
1209 return setError(QStringLiteral("Animation %1 channel references invalid sampler %2")
1210 .arg(document->animations.size())
1211 .arg(channel.sampler));
1212 if (channel.targetNode >= nodeCount)
1213 return setError(QStringLiteral("Animation %1 channel references invalid node %2")
1214 .arg(document->animations.size())
1215 .arg(channel.targetNode));
1216 animation.channels.append(channel);
1217 }
1218
1219 document->animations.append(animation);
1220 }
1221
1222 // scenes
1223 for (const auto &value : root.value(QLatin1String("scenes")).toArray()) {
1224 const QJsonObject object = value.toObject();
1225 Scene scene;
1226 scene.name = object.value(QLatin1String("name")).toString();
1227 QSet<int> seenRoots;
1228 for (const auto &nodeIndex : object.value(QLatin1String("nodes")).toArray()) {
1229 const int index = nodeIndex.toInt(-1);
1230 if (index < 0 || index >= nodeCount)
1231 return setError(QStringLiteral("Scene %1 references invalid node %2")
1232 .arg(document->scenes.size()).arg(index));
1233 // Scene nodes must be distinct root nodes; a repeated node, or one
1234 // that is also somebody's child, would have its subtree emitted
1235 // more than once. Skip the offending entry rather than failing, so
1236 // that the scene still contains everything it should, once.
1237 if (parentCount[index] != 0 || seenRoots.contains(index)) {
1238 qCWarning(lcQuick3DGltf) << "Scene" << document->scenes.size() << "references node" << index
1239 << "which is not a unique root node; ignoring it";
1240 continue;
1241 }
1242 seenRoots.insert(index);
1243 scene.nodes.append(index);
1244 }
1245 document->scenes.append(scene);
1246 }
1247 document->scene = root.value(QLatin1String("scene")).toInt(-1);
1248 if (document->scene >= document->scenes.size())
1249 return setError(QStringLiteral("Document references invalid default scene %1").arg(document->scene));
1250
1251 return true;
1252}
1253
1254QT_END_NAMESPACE
Combined button and popup list for selecting options.